diff --git a/src/features/attributes/attributeGroups/attributeGroup.model.js b/src/features/attributes/attributeGroups/attributeGroup.model.js index 89bd10b..230063e 100644 --- a/src/features/attributes/attributeGroups/attributeGroup.model.js +++ b/src/features/attributes/attributeGroups/attributeGroup.model.js @@ -27,6 +27,10 @@ export default (sequelize) => { primaryKey: true, allowNull: false }, + tenant_id: { + type: DataTypes.INTEGER, + allowNull: true + }, code: { type: DataTypes.STRING(50), allowNull: false, diff --git a/src/features/attributes/attributeGroups/attributeGroup.repository.js b/src/features/attributes/attributeGroups/attributeGroup.repository.js index 8564385..0b99816 100644 --- a/src/features/attributes/attributeGroups/attributeGroup.repository.js +++ b/src/features/attributes/attributeGroups/attributeGroup.repository.js @@ -1,8 +1,12 @@ import { models } from '../../../shared/database/models.js'; +import { applyTenantScope } from '../../../utils/helpers/common.helper.js'; export class AttributeGroupRepository { async findAll(options = {}, context = {}) { + const where = applyTenantScope(options.where || {}, context); return await models.AttributeGroup.findAll({ + ...options, + where, include: [ { model: models.Attribute, @@ -12,28 +16,30 @@ export class AttributeGroupRepository { ], order: [ ['name', 'ASC'] - ], - ...options + ] }); } async findById(id, options = {}, context = {}) { - return await models.AttributeGroup.findByPk(id, { + const where = applyTenantScope({ id }, context); + return await models.AttributeGroup.findOne({ + ...options, + where, include: [ { model: models.Attribute, as: 'attributes', through: { attributes: ['display_order'] } } - ], - ...options + ] }); } async findByCode(code, options = {}, context = {}) { + const where = applyTenantScope({ code }, context); return await models.AttributeGroup.findOne({ - where: { code }, - ...options + ...options, + where }); } diff --git a/src/features/attributes/attributeSets/attributeSet.model.js b/src/features/attributes/attributeSets/attributeSet.model.js index a5e5c12..5299e37 100644 --- a/src/features/attributes/attributeSets/attributeSet.model.js +++ b/src/features/attributes/attributeSets/attributeSet.model.js @@ -20,6 +20,10 @@ export default (sequelize) => { primaryKey: true, allowNull: false }, + tenant_id: { + type: DataTypes.INTEGER, + allowNull: true + }, code: { type: DataTypes.STRING(50), allowNull: false, diff --git a/src/features/attributes/attributeSets/attributeSet.repository.js b/src/features/attributes/attributeSets/attributeSet.repository.js index 0076910..c01b006 100644 --- a/src/features/attributes/attributeSets/attributeSet.repository.js +++ b/src/features/attributes/attributeSets/attributeSet.repository.js @@ -1,8 +1,12 @@ import { models } from '../../../shared/database/models.js'; +import { applyTenantScope } from '../../../utils/helpers/common.helper.js'; export class AttributeSetRepository { async findAll(options = {}, context = {}) { + const where = applyTenantScope(options.where || {}, context); return await models.AttributeSet.findAll({ + ...options, + where, include: [ { model: models.AttributeGroup, @@ -26,13 +30,15 @@ export class AttributeSetRepository { ], order: [ ['name', 'ASC'] - ], - ...options + ] }); } async findById(id, options = {}, context = {}) { - return await models.AttributeSet.findByPk(id, { + const where = applyTenantScope({ id }, context); + return await models.AttributeSet.findOne({ + ...options, + where, include: [ { model: models.AttributeGroup, @@ -53,15 +59,15 @@ export class AttributeSetRepository { } ] } - ], - ...options + ] }); } async findByCode(code, options = {}, context = {}) { + const where = applyTenantScope({ code }, context); return await models.AttributeSet.findOne({ - where: { code }, - ...options + ...options, + where }); } diff --git a/src/features/attributes/attributes/attribute.model.js b/src/features/attributes/attributes/attribute.model.js index 152456e..a16a7ac 100644 --- a/src/features/attributes/attributes/attribute.model.js +++ b/src/features/attributes/attributes/attribute.model.js @@ -40,6 +40,10 @@ export default (sequelize) => { primaryKey: true, allowNull: false }, + tenant_id: { + type: DataTypes.INTEGER, + allowNull: true + }, code: { type: DataTypes.STRING(50), allowNull: false, diff --git a/src/features/attributes/attributes/attribute.repository.js b/src/features/attributes/attributes/attribute.repository.js index 8921238..8400844 100644 --- a/src/features/attributes/attributes/attribute.repository.js +++ b/src/features/attributes/attributes/attribute.repository.js @@ -1,12 +1,18 @@ import { models } from '../../../shared/database/models.js'; +import { applyTenantScope } from '../../../utils/helpers/common.helper.js'; export class AttributeRepository { async findAll(options = {}, context = {}) { - return await models.Attribute.findAll(options); + const queryOptions = { + ...options, + where: applyTenantScope(options.where || {}, context) + }; + return await models.Attribute.findAll(queryOptions); } async findById(id, options = {}, context = {}) { - return await models.Attribute.findByPk(id, options); + const where = applyTenantScope({ id }, context); + return await models.Attribute.findOne({ ...options, where }); } async create(data, options = {}, context = {}) { diff --git a/src/features/auditLogs/auditLogs/auditLog.model.js b/src/features/auditLogs/auditLogs/auditLog.model.js index 577579e..086b1ac 100644 --- a/src/features/auditLogs/auditLogs/auditLog.model.js +++ b/src/features/auditLogs/auditLogs/auditLog.model.js @@ -14,6 +14,10 @@ export default (sequelize) => { primaryKey: true, allowNull: false }, + tenant_id: { + type: DataTypes.INTEGER, + allowNull: true + }, action: { type: DataTypes.STRING, allowNull: false diff --git a/src/features/auditLogs/auditLogs/auditLog.repository.js b/src/features/auditLogs/auditLogs/auditLog.repository.js index 6d48b43..9d0135e 100644 --- a/src/features/auditLogs/auditLogs/auditLog.repository.js +++ b/src/features/auditLogs/auditLogs/auditLog.repository.js @@ -1,26 +1,30 @@ import { models } from '../../../shared/database/models.js'; +import { applyTenantScope } from '../../../utils/helpers/common.helper.js'; export class AuditLogRepository { - async findAll(options = {}) { - return await models.AuditLog.findAll(options); + async findAll(options = {}, context = {}) { + const where = applyTenantScope(options.where || {}, context); + return await models.AuditLog.findAll({ ...options, where }); } - async findById(id, options = {}) { - return await models.AuditLog.findByPk(id, options); + async findById(id, options = {}, context = {}) { + const where = applyTenantScope({ id }, context); + return await models.AuditLog.findOne({ ...options, where }); } - async create(data, options = {}) { - return await models.AuditLog.create(data, options); + async create(data, options = {}, context = {}) { + const tenantId = context.tenant_id || context.tenantId || null; + return await models.AuditLog.create({ ...data, tenant_id: tenantId }, options); } - async update(id, data, options = {}) { - const record = await this.findById(id, options); + async update(id, data, options = {}, context = {}) { + const record = await this.findById(id, options, context); if (!record) return null; return await record.update(data, options); } - async delete(id, options = {}) { - const record = await this.findById(id, options); + async delete(id, options = {}, context = {}) { + const record = await this.findById(id, options, context); if (!record) return false; await record.destroy(options); return true; diff --git a/src/features/authentication/users/user.routes.js b/src/features/authentication/users/user.routes.js index df697f8..fe25833 100644 --- a/src/features/authentication/users/user.routes.js +++ b/src/features/authentication/users/user.routes.js @@ -79,10 +79,13 @@ router.get( * 400: * description: Validation Error */ +import { checkUserQuota } from '../../../shared/middleware/quota.middleware.js'; + router.post( '/invite', authenticate, authorize(['settings.users']), + checkUserQuota, controller.invite ); diff --git a/src/features/channels/channelTypes/channelType.model.js b/src/features/channels/channelTypes/channelType.model.js index 4ab3d75..741fdce 100644 --- a/src/features/channels/channelTypes/channelType.model.js +++ b/src/features/channels/channelTypes/channelType.model.js @@ -17,6 +17,10 @@ export default (sequelize) => { primaryKey: true, allowNull: false }, + tenant_id: { + type: DataTypes.INTEGER, + allowNull: true + }, code: { type: DataTypes.STRING(50), allowNull: false, diff --git a/src/features/channels/channelTypes/channelType.repository.js b/src/features/channels/channelTypes/channelType.repository.js index 81385b3..f073029 100644 --- a/src/features/channels/channelTypes/channelType.repository.js +++ b/src/features/channels/channelTypes/channelType.repository.js @@ -1,26 +1,30 @@ import { models } from '../../../shared/database/models.js'; +import { applyTenantScope } from '../../../utils/helpers/common.helper.js'; export class ChannelTypeRepository { - async findAll(options = {}) { - return await models.ChannelType.findAll(options); + async findAll(options = {}, context = {}) { + const where = applyTenantScope(options.where || {}, context); + return await models.ChannelType.findAll({ ...options, where }); } - async findById(id, options = {}) { - return await models.ChannelType.findByPk(id, options); + async findById(id, options = {}, context = {}) { + const where = applyTenantScope({ id }, context); + return await models.ChannelType.findOne({ ...options, where }); } - async create(data, options = {}) { - return await models.ChannelType.create(data, options); + async create(data, options = {}, context = {}) { + const tenantId = context.tenant_id || context.tenantId || null; + return await models.ChannelType.create({ ...data, tenant_id: tenantId }, options); } - async update(id, data, options = {}) { - const record = await this.findById(id, options); + async update(id, data, options = {}, context = {}) { + const record = await this.findById(id, options, context); if (!record) return null; return await record.update(data, options); } - async delete(id, options = {}) { - const record = await this.findById(id, options); + async delete(id, options = {}, context = {}) { + const record = await this.findById(id, options, context); if (!record) return false; await record.destroy(options); return true; diff --git a/src/features/channels/channels/channel.model.js b/src/features/channels/channels/channel.model.js index b550c15..7e81206 100644 --- a/src/features/channels/channels/channel.model.js +++ b/src/features/channels/channels/channel.model.js @@ -17,6 +17,10 @@ export default (sequelize) => { primaryKey: true, allowNull: false }, + tenant_id: { + type: DataTypes.INTEGER, + allowNull: true + }, type_id: { type: DataTypes.UUID, allowNull: true diff --git a/src/features/channels/channels/channel.repository.js b/src/features/channels/channels/channel.repository.js index c86e9bc..10fd789 100644 --- a/src/features/channels/channels/channel.repository.js +++ b/src/features/channels/channels/channel.repository.js @@ -1,26 +1,30 @@ import { models } from '../../../shared/database/models.js'; +import { applyTenantScope } from '../../../utils/helpers/common.helper.js'; export class ChannelRepository { - async findAll(options = {}) { - return await models.Channel.findAll(options); + async findAll(options = {}, context = {}) { + const where = applyTenantScope(options.where || {}, context); + return await models.Channel.findAll({ ...options, where }); } - async findById(id, options = {}) { - return await models.Channel.findByPk(id, options); + async findById(id, options = {}, context = {}) { + const where = applyTenantScope({ id }, context); + return await models.Channel.findOne({ ...options, where }); } - async create(data, options = {}) { - return await models.Channel.create(data, options); + async create(data, options = {}, context = {}) { + const tenantId = context.tenant_id || context.tenantId || null; + return await models.Channel.create({ ...data, tenant_id: tenantId }, options); } - async update(id, data, options = {}) { - const record = await this.findById(id, options); + async update(id, data, options = {}, context = {}) { + const record = await this.findById(id, options, context); if (!record) return null; return await record.update(data, options); } - async delete(id, options = {}) { - const record = await this.findById(id, options); + async delete(id, options = {}, context = {}) { + const record = await this.findById(id, options, context); if (!record) return false; await record.destroy(options); return true; diff --git a/src/features/media/assetFamilies/assetFamily.model.js b/src/features/media/assetFamilies/assetFamily.model.js index d247ca1..9793103 100644 --- a/src/features/media/assetFamilies/assetFamily.model.js +++ b/src/features/media/assetFamilies/assetFamily.model.js @@ -12,6 +12,10 @@ export default (sequelize) => { primaryKey: true, allowNull: false }, + tenant_id: { + type: DataTypes.INTEGER, + allowNull: true + }, name: { type: DataTypes.STRING, allowNull: false diff --git a/src/features/media/assetFamilies/assetFamily.repository.js b/src/features/media/assetFamilies/assetFamily.repository.js index ccc2aab..849848d 100644 --- a/src/features/media/assetFamilies/assetFamily.repository.js +++ b/src/features/media/assetFamilies/assetFamily.repository.js @@ -1,4 +1,5 @@ import { models } from '../../../shared/database/models.js'; +import { applyTenantScope } from '../../../utils/helpers/common.helper.js'; export class AssetFamilyRepository { async ensureTable() { @@ -11,37 +12,43 @@ export class AssetFamilyRepository { } } - async findAll(options = {}) { + async findAll(options = {}, context = {}) { + const where = applyTenantScope(options.where || {}, context); + const queryOptions = { ...options, where }; try { - return await models.AssetFamily.findAll(options); + return await models.AssetFamily.findAll(queryOptions); } catch (err) { if (err.name === 'SequelizeDatabaseError' || err.message?.includes('no such table') || err.message?.includes('does not exist')) { await this.ensureTable(); - return await models.AssetFamily.findAll(options); + return await models.AssetFamily.findAll(queryOptions); } throw err; } } - async findById(id, options = {}) { + async findById(id, options = {}, context = {}) { + const where = applyTenantScope({ id }, context); + const queryOptions = { ...options, where }; try { - return await models.AssetFamily.findByPk(id, options); + return await models.AssetFamily.findOne(queryOptions); } catch (err) { if (err.name === 'SequelizeDatabaseError' || err.message?.includes('no such table') || err.message?.includes('does not exist')) { await this.ensureTable(); - return await models.AssetFamily.findByPk(id, options); + return await models.AssetFamily.findOne(queryOptions); } throw err; } } - async create(data, options = {}) { + async create(data, options = {}, context = {}) { + const tenantId = context.tenant_id || context.tenantId || null; + const dataWithTenant = { ...data, tenant_id: tenantId }; try { - return await models.AssetFamily.create(data, options); + return await models.AssetFamily.create(dataWithTenant, options); } catch (err) { if (err.name === 'SequelizeDatabaseError' || err.message?.includes('no such table') || err.message?.includes('does not exist')) { await this.ensureTable(); - return await models.AssetFamily.create(data, options); + return await models.AssetFamily.create(dataWithTenant, options); } throw err; } diff --git a/src/features/media/assetTypes/assetType.model.js b/src/features/media/assetTypes/assetType.model.js index c94f701..04504f7 100644 --- a/src/features/media/assetTypes/assetType.model.js +++ b/src/features/media/assetTypes/assetType.model.js @@ -12,6 +12,10 @@ export default (sequelize) => { primaryKey: true, allowNull: false }, + tenant_id: { + type: DataTypes.INTEGER, + allowNull: true + }, name: { type: DataTypes.STRING, allowNull: false diff --git a/src/features/media/assetTypes/assetType.repository.js b/src/features/media/assetTypes/assetType.repository.js index bbc5e34..87836e0 100644 --- a/src/features/media/assetTypes/assetType.repository.js +++ b/src/features/media/assetTypes/assetType.repository.js @@ -1,26 +1,30 @@ import { models } from '../../../shared/database/models.js'; +import { applyTenantScope } from '../../../utils/helpers/common.helper.js'; export class AssetTypeRepository { - async findAll(options = {}) { - return await models.AssetType.findAll(options); + async findAll(options = {}, context = {}) { + const where = applyTenantScope(options.where || {}, context); + return await models.AssetType.findAll({ ...options, where }); } - async findById(id, options = {}) { - return await models.AssetType.findByPk(id, options); + async findById(id, options = {}, context = {}) { + const where = applyTenantScope({ id }, context); + return await models.AssetType.findOne({ ...options, where }); } - async create(data, options = {}) { - return await models.AssetType.create(data, options); + async create(data, options = {}, context = {}) { + const tenantId = context.tenant_id || context.tenantId || null; + return await models.AssetType.create({ ...data, tenant_id: tenantId }, options); } - async update(id, data, options = {}) { - const record = await this.findById(id, options); + async update(id, data, options = {}, context = {}) { + const record = await this.findById(id, options, context); if (!record) return null; return await record.update(data, options); } - async delete(id, options = {}) { - const record = await this.findById(id, options); + async delete(id, options = {}, context = {}) { + const record = await this.findById(id, options, context); if (!record) return false; await record.destroy(options); return true; diff --git a/src/features/media/assets/assetFolder.model.js b/src/features/media/assets/assetFolder.model.js index 0087a00..a785026 100644 --- a/src/features/media/assets/assetFolder.model.js +++ b/src/features/media/assets/assetFolder.model.js @@ -25,6 +25,10 @@ export default (sequelize) => { primaryKey: true, allowNull: false }, + tenant_id: { + type: DataTypes.INTEGER, + allowNull: true + }, name: { type: DataTypes.STRING(100), allowNull: false diff --git a/src/features/products/products/product.routes.js b/src/features/products/products/product.routes.js index dd1b004..40419c6 100644 --- a/src/features/products/products/product.routes.js +++ b/src/features/products/products/product.routes.js @@ -65,10 +65,13 @@ router.get( * 201: * description: Success */ +import { checkProductQuota } from '../../../shared/middleware/quota.middleware.js'; + router.post( '/', authenticate, authorize(['products.items']), + checkProductQuota, createValidation, validate, audit('CREATE_PRODUCT'), diff --git a/src/features/variants/variants/variant.model.js b/src/features/variants/variants/variant.model.js index 6dd0e5e..c1cea73 100644 --- a/src/features/variants/variants/variant.model.js +++ b/src/features/variants/variants/variant.model.js @@ -30,6 +30,10 @@ export default (sequelize) => { primaryKey: true, allowNull: false }, + tenant_id: { + type: DataTypes.INTEGER, + allowNull: true + }, product_id: { type: DataTypes.UUID, allowNull: false diff --git a/src/features/variants/variants/variant.repository.js b/src/features/variants/variants/variant.repository.js index 6c79ed5..007c745 100644 --- a/src/features/variants/variants/variant.repository.js +++ b/src/features/variants/variants/variant.repository.js @@ -1,8 +1,12 @@ import { models } from '../../../shared/database/models.js'; +import { applyTenantScope } from '../../../utils/helpers/common.helper.js'; export class VariantRepository { async findAll(options = {}, context = {}) { + const where = applyTenantScope(options.where || {}, context); return await models.Variant.findAll({ + ...options, + where, include: [ { model: models.VariantValue, @@ -23,13 +27,15 @@ export class VariantRepository { ], order: [ ['sku', 'ASC'] - ], - ...options + ] }); } async findById(id, options = {}, context = {}) { - return await models.Variant.findByPk(id, { + const where = applyTenantScope({ id }, context); + return await models.Variant.findOne({ + ...options, + where, include: [ { model: models.VariantValue, @@ -47,21 +53,21 @@ export class VariantRepository { as: 'product', attributes: ['id', 'name', 'code'] } - ], - ...options + ] }); } async findBySku(sku, options = {}, context = {}) { + const where = applyTenantScope({ sku }, context); return await models.Variant.findOne({ - where: { sku }, + ...options, + where, include: [ { model: models.VariantValue, as: 'values' } - ], - ...options + ] }); } diff --git a/src/features/workflows/workflow.model.js b/src/features/workflows/workflow.model.js index 9174d8a..c6965ef 100644 --- a/src/features/workflows/workflow.model.js +++ b/src/features/workflows/workflow.model.js @@ -14,6 +14,10 @@ export default (sequelize) => { primaryKey: true, allowNull: false }, + tenant_id: { + type: DataTypes.INTEGER, + allowNull: true + }, code: { type: DataTypes.STRING(50), allowNull: false, diff --git a/src/features/workflows/workflow.repository.js b/src/features/workflows/workflow.repository.js index 3246a9d..799c24c 100644 --- a/src/features/workflows/workflow.repository.js +++ b/src/features/workflows/workflow.repository.js @@ -1,16 +1,20 @@ import { models } from '../../shared/database/models.js'; +import { applyTenantScope } from '../../utils/helpers/common.helper.js'; export class WorkflowRepository { async findAll(options = {}, context = {}) { - return await models.WorkflowRegistry.findAll(options); + const where = applyTenantScope(options.where || {}, context); + return await models.WorkflowRegistry.findAll({ ...options, where }); } async findById(id, options = {}, context = {}) { - return await models.WorkflowRegistry.findByPk(id, options); + const where = applyTenantScope({ id }, context); + return await models.WorkflowRegistry.findOne({ ...options, where }); } async create(data, options = {}, context = {}) { - return await models.WorkflowRegistry.create(data, options); + const tenantId = context.tenant_id || context.tenantId || null; + return await models.WorkflowRegistry.create({ ...data, tenant_id: tenantId }, options); } async update(id, data, options = {}, context = {}) { diff --git a/src/shared/middleware/quota.middleware.js b/src/shared/middleware/quota.middleware.js new file mode 100644 index 0000000..c3ce9b4 --- /dev/null +++ b/src/shared/middleware/quota.middleware.js @@ -0,0 +1,64 @@ +import { models } from '../database/models.js'; + +export const checkProductQuota = async (req, res, next) => { + try { + // Platform superadmins bypass quota limits + if (req.user && req.user.user_type === 'platform') { + return next(); + } + + const tenantId = req.user?.tenant_id; + if (!tenantId) { + return next(); + } + + const tenant = await models.Tenant.findByPk(tenantId); + if (!tenant) { + return next(); + } + + const currentCount = await models.Product.count({ where: { tenant_id: tenantId } }); + if (tenant.max_products && currentCount >= tenant.max_products) { + return res.status(402).json({ + success: false, + message: `Plan limit reached: Your subscription tier (${tenant.plan_name}) allows up to ${tenant.max_products} products. Please upgrade your plan in Platform Control to add more products.`, + error_code: 'PLAN_QUOTA_EXCEEDED' + }); + } + + next(); + } catch (error) { + next(error); + } +}; + +export const checkUserQuota = async (req, res, next) => { + try { + if (req.user && req.user.user_type === 'platform') { + return next(); + } + + const tenantId = req.user?.tenant_id; + if (!tenantId) { + return next(); + } + + const tenant = await models.Tenant.findByPk(tenantId); + if (!tenant) { + return next(); + } + + const currentCount = await models.User.count({ where: { tenant_id: tenantId } }); + if (tenant.max_users && currentCount >= tenant.max_users) { + return res.status(402).json({ + success: false, + message: `Plan limit reached: Your subscription tier (${tenant.plan_name}) allows up to ${tenant.max_users} users. Please upgrade your plan in Platform Control to invite more team members.`, + error_code: 'PLAN_USER_QUOTA_EXCEEDED' + }); + } + + next(); + } catch (error) { + next(error); + } +};