feat(tenant-engine): enhance tenant provisioning with subscription quotas, baseline roles, email onboarding & fix double-hashing bug

This commit is contained in:
Inamul-hasan-tec
2026-08-08 11:09:40 +05:30
parent 24caaaf93f
commit 95b0753f30
3 changed files with 112 additions and 5 deletions
@@ -93,13 +93,13 @@ export default (sequelize) => {
updatedAt: 'updated_at',
hooks: {
beforeCreate: async (user) => {
if (user.password_hash) {
if (user.password_hash && !user.password_hash.startsWith('$2b$') && !user.password_hash.startsWith('$2a$')) {
const salt = await bcrypt.genSalt(10);
user.password_hash = await bcrypt.hash(user.password_hash, salt);
}
},
beforeUpdate: async (user) => {
if (user.changed('password_hash')) {
if (user.changed('password_hash') && !user.password_hash.startsWith('$2b$') && !user.password_hash.startsWith('$2a$')) {
const salt = await bcrypt.genSalt(10);
user.password_hash = await bcrypt.hash(user.password_hash, salt);
}
@@ -2,8 +2,15 @@ import { models, sequelize } from '../../../shared/database/models.js';
import { ApiError } from '../../../utils/helpers/ApiError.utils.js';
import { generateUniqueCode } from '../../../utils/helpers/code.utils.js';
import { generateToken } from '../../../utils/helpers/jwt.utils.js';
import { sendEmail } from '../../../shared/utils/email.js';
import bcrypt from 'bcrypt';
const PLAN_LIMITS = {
STARTER: { max_users: 10, max_products: 5000, storage_limit_mb: 5000 },
PROFESSIONAL: { max_users: 50, max_products: 50000, storage_limit_mb: 50000 },
ENTERPRISE: { max_users: 500, max_products: 500000, storage_limit_mb: 500000 }
};
export class PlatformService {
async getTenants(query = {}) {
const where = {};
@@ -39,12 +46,13 @@ export class PlatformService {
}
async provisionTenant(data, context = {}) {
const { tenant_name, domain, contact_email, admin_name, admin_email, admin_password } = data;
const { tenant_name, domain, contact_email, plan_name = 'STARTER', admin_name, admin_email, admin_password } = data;
if (!tenant_name || !contact_email) {
throw new ApiError(400, 'Tenant name and contact email are required');
}
const selectedPlan = PLAN_LIMITS[plan_name.toUpperCase()] || PLAN_LIMITS.STARTER;
const transaction = await sequelize.transaction();
try {
@@ -56,16 +64,20 @@ export class PlatformService {
const tenantCode = await generateUniqueCode(models.Tenant, tenant_name, 'tenant_code', transaction);
// 2. Create Tenant Record
// 2. Create Tenant Record with Quota Limits
const tenant = await models.Tenant.create({
tenant_name,
tenant_code: tenantCode,
domain: domain ? domain.toLowerCase().trim() : null,
contact_email,
plan_name: plan_name.toUpperCase(),
max_users: selectedPlan.max_users,
max_products: selectedPlan.max_products,
storage_limit_mb: selectedPlan.storage_limit_mb,
status: true
}, { transaction });
// 3. Create Tenant Admin Role for the new Tenant
// 3. Create Baseline Tenant Roles for the new Tenant
const tenantAdminRole = await models.Role.create({
tenant_id: tenant.id,
role_code: 'TENANT_ADMIN',
@@ -76,6 +88,26 @@ export class PlatformService {
status: true
}, { transaction });
const catalogManagerRole = await models.Role.create({
tenant_id: tenant.id,
role_code: 'CATALOG_MANAGER',
role_name: 'Catalog Manager',
description: 'Manage products, families, categories, attributes, and digital assets',
role_type: 'tenant',
is_system_role: false,
status: true
}, { transaction });
const catalogViewerRole = await models.Role.create({
tenant_id: tenant.id,
role_code: 'CATALOG_VIEWER',
role_name: 'Catalog Viewer',
description: 'Read-only view access across tenant catalog items and assets',
role_type: 'tenant',
is_system_role: false,
status: true
}, { transaction });
// Assign all non-platform feature nodes to Tenant Admin role
const featureNodes = await models.PermissionNode.findAll({
where: { node_type: 'feature' },
@@ -84,6 +116,7 @@ export class PlatformService {
for (const node of featureNodes) {
if (!node.node_code.startsWith('platform.')) {
// TENANT_ADMIN gets full access
await models.RolePermission.create({
role_id: tenantAdminRole.id,
node_id: node.id,
@@ -95,6 +128,34 @@ export class PlatformService {
can_export: true,
can_import: true
}, { transaction });
// CATALOG_MANAGER gets catalog access
if (node.node_code.startsWith('products') || node.node_code.startsWith('masters')) {
await models.RolePermission.create({
role_id: catalogManagerRole.id,
node_id: node.id,
can_view: true,
can_create: true,
can_edit: true,
can_delete: false,
can_alter: false,
can_export: true,
can_import: true
}, { transaction });
}
// CATALOG_VIEWER gets view-only
await models.RolePermission.create({
role_id: catalogViewerRole.id,
node_id: node.id,
can_view: true,
can_create: false,
can_edit: false,
can_delete: false,
can_alter: false,
can_export: false,
can_import: false
}, { transaction });
}
}
@@ -122,6 +183,29 @@ export class PlatformService {
await transaction.commit();
// 5. Send Onboarding Welcome Email asynchronously
if (adminUser) {
sendEmail({
to: adminUser.email,
subject: `Welcome to Maskan PIM — ${tenant.tenant_name} Workspace Provisioned`,
html: `
<div style="font-family: Arial, sans-serif; padding: 20px; color: #333;">
<h2 style="color: #4f46e5;">Welcome to Maskan PIM!</h2>
<p>Your enterprise product information workspace for <strong>${tenant.tenant_name}</strong> has been successfully provisioned.</p>
<h3>Workspace Details</h3>
<ul>
<li><strong>Tenant Code:</strong> ${tenant.tenant_code}</li>
<li><strong>Plan:</strong> ${tenant.plan_name} (${tenant.max_products.toLocaleString()} product limit)</li>
<li><strong>Admin Email:</strong> ${adminUser.email}</li>
</ul>
<p>You can now log in at your workspace URL to manage your catalog items and team members.</p>
<br/>
<p>Best regards,<br/><strong>Maskan PIM Platform Team</strong></p>
</div>
`
}).catch(err => console.error('Failed to send tenant welcome email:', err.message));
}
return {
tenant: {
id: tenant.id,
@@ -129,6 +213,10 @@ export class PlatformService {
tenant_code: tenant.tenant_code,
domain: tenant.domain,
contact_email: tenant.contact_email,
plan_name: tenant.plan_name,
max_users: tenant.max_users,
max_products: tenant.max_products,
storage_limit_mb: tenant.storage_limit_mb,
status: tenant.status
},
admin: adminUser ? {
@@ -37,6 +37,25 @@ export default (sequelize) => {
status: {
type: DataTypes.BOOLEAN,
defaultValue: true
},
plan_name: {
type: DataTypes.STRING(50),
defaultValue: 'STARTER'
},
max_users: {
type: DataTypes.INTEGER,
defaultValue: 10
},
max_products: {
type: DataTypes.INTEGER,
defaultValue: 5000
},
storage_limit_mb: {
type: DataTypes.INTEGER,
defaultValue: 5000
},
subscription_expires_at: {
type: DataTypes.DATE
}
}, {
sequelize,