feat(saas): complete 100% multi-tenant architecture across all 21 models with repository scoping & plan quota guards
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {}) {
|
||||
|
||||
@@ -14,6 +14,10 @@ export default (sequelize) => {
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
tenant_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
},
|
||||
action: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -12,6 +12,10 @@ export default (sequelize) => {
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
tenant_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ export default (sequelize) => {
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
tenant_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {}) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user