solved merge conflicts
This commit is contained in:
+19
-19
@@ -1,21 +1,21 @@
|
||||
PORT=5000
|
||||
NODE_ENV=development
|
||||
CORS_ORIGIN=http://localhost:5173
|
||||
JWT_SECRET=supersecretjwtkeythatislongandsecure
|
||||
JWT_EXPIRES_IN=7d
|
||||
# PORT=5000
|
||||
# NODE_ENV=development
|
||||
# CORS_ORIGIN=http://localhost:5173
|
||||
# JWT_SECRET=supersecretjwtkeythatislongandsecure
|
||||
# JWT_EXPIRES_IN=7d
|
||||
|
||||
DB_HOST=106.51.105.22
|
||||
DB_PORT=5432
|
||||
DB_NAME=pc_dev
|
||||
DB_USER=pc_user
|
||||
DB_PASSWORD="#TpW@%a&b$[zm"
|
||||
DB_DIALECT=postgres
|
||||
# DB_HOST=106.51.105.22
|
||||
# DB_PORT=5432
|
||||
# DB_NAME=pc_dev
|
||||
# DB_USER=pc_user
|
||||
# DB_PASSWORD="#TpW@%a&b$[zm"
|
||||
# DB_DIALECT=postgres
|
||||
|
||||
# IMPORTANT: Gmail SMTP requires an App Password, NOT your regular password.
|
||||
# Go to: https://myaccount.google.com -> Security -> 2-Step Verification -> App Passwords
|
||||
# Generate a 16-character App Password and paste it below (no spaces).
|
||||
EMAIL_HOST=smtp.gmail.com
|
||||
EMAIL_PORT=587
|
||||
EMAIL_USER=pmahboob001@gmail.com
|
||||
EMAIL_PASSWORD=hlwg yfgi xmkn lqmg
|
||||
EMAIL_FROM=Maskan PIM <pmahboob001@gmail.com>
|
||||
# # IMPORTANT: Gmail SMTP requires an App Password, NOT your regular password.
|
||||
# # Go to: https://myaccount.google.com -> Security -> 2-Step Verification -> App Passwords
|
||||
# # Generate a 16-character App Password and paste it below (no spaces).
|
||||
# EMAIL_HOST=smtp.gmail.com
|
||||
# EMAIL_PORT=587
|
||||
# EMAIL_USER=pmahboob001@gmail.com
|
||||
# EMAIL_PASSWORD=hlwg yfgi xmkn lqmg
|
||||
# EMAIL_FROM=Maskan PIM <pmahboob001@gmail.com>
|
||||
|
||||
+11
-11
@@ -1,12 +1,12 @@
|
||||
PORT=5000
|
||||
NODE_ENV=development
|
||||
CORS_ORIGIN=http://localhost:3000
|
||||
JWT_SECRET=supersecretjwtkeythatislongandsecure
|
||||
JWT_EXPIRES_IN=7d
|
||||
# PORT=5000
|
||||
# NODE_ENV=development
|
||||
# CORS_ORIGIN=http://localhost:3000
|
||||
# JWT_SECRET=supersecretjwtkeythatislongandsecure
|
||||
# JWT_EXPIRES_IN=7d
|
||||
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=5432
|
||||
DB_USER=postgres
|
||||
DB_PASS=postgres
|
||||
DB_NAME=maskan_pim
|
||||
DB_DIALECT=postgres
|
||||
# DB_HOST=127.0.0.1
|
||||
# DB_PORT=5432
|
||||
# DB_USER=postgres
|
||||
# DB_PASS=postgres
|
||||
# DB_NAME=maskan_pim
|
||||
# DB_DIALECT=postgres
|
||||
|
||||
+4
-4
@@ -4,11 +4,11 @@ CORS_ORIGIN=http://localhost:5173
|
||||
JWT_SECRET=supersecretjwtkeythatislongandsecure
|
||||
JWT_EXPIRES_IN=7d
|
||||
|
||||
DB_HOST=106.51.105.22
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=5432
|
||||
DB_NAME=pc_local
|
||||
DB_USER=pc_user
|
||||
DB_PASSWORD="#TpW@%a&b$[zm"
|
||||
DB_NAME=Pim_Maskan
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=postgres
|
||||
DB_DIALECT=postgres
|
||||
|
||||
# IMPORTANT: Gmail SMTP requires an App Password, NOT your regular password.
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
PORT=5000
|
||||
NODE_ENV=test
|
||||
CORS_ORIGIN=http://localhost:5173
|
||||
JWT_SECRET=supersecretjwtkeythatislongandsecure
|
||||
JWT_EXPIRES_IN=7d
|
||||
# PORT=5000
|
||||
# NODE_ENV=test
|
||||
# CORS_ORIGIN=http://localhost:5173
|
||||
# JWT_SECRET=supersecretjwtkeythatislongandsecure
|
||||
# JWT_EXPIRES_IN=7d
|
||||
|
||||
DB_HOST=106.51.105.22
|
||||
DB_PORT=5432
|
||||
DB_NAME=pc_test
|
||||
DB_USER=pc_user
|
||||
DB_PASSWORD="#TpW@%a&b$[zm"
|
||||
DB_DIALECT=postgres
|
||||
# DB_HOST=106.51.105.22
|
||||
# DB_PORT=5432
|
||||
# DB_NAME=pc_test
|
||||
# DB_USER=pc_user
|
||||
# DB_PASSWORD="#TpW@%a&b$[zm"
|
||||
# DB_DIALECT=postgres
|
||||
|
||||
# IMPORTANT: Gmail SMTP requires an App Password, NOT your regular password.
|
||||
# Go to: https://myaccount.google.com -> Security -> 2-Step Verification -> App Passwords
|
||||
# Generate a 16-character App Password and paste it below (no spaces).
|
||||
EMAIL_HOST=smtp.gmail.com
|
||||
EMAIL_PORT=587
|
||||
EMAIL_USER=pmahboob001@gmail.com
|
||||
EMAIL_PASSWORD=hlwg yfgi xmkn lqmg
|
||||
EMAIL_FROM=Maskan PIM <pmahboob001@gmail.com>
|
||||
# # IMPORTANT: Gmail SMTP requires an App Password, NOT your regular password.
|
||||
# # Go to: https://myaccount.google.com -> Security -> 2-Step Verification -> App Passwords
|
||||
# # Generate a 16-character App Password and paste it below (no spaces).
|
||||
# EMAIL_HOST=smtp.gmail.com
|
||||
# EMAIL_PORT=587
|
||||
# EMAIL_USER=pmahboob001@gmail.com
|
||||
# EMAIL_PASSWORD=hlwg yfgi xmkn lqmg
|
||||
# EMAIL_FROM=Maskan PIM <pmahboob001@gmail.com>
|
||||
|
||||
@@ -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 './attributeGroup.validation.js';
|
||||
|
||||
const router = Router();
|
||||
@@ -26,7 +26,7 @@ const router = Router();
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['read:attributes']),
|
||||
authorize(['products.attributes']),
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
@@ -47,7 +47,7 @@ router.get(
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['read:attributes']),
|
||||
authorize(['products.attributes']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
controller.getById
|
||||
@@ -66,7 +66,7 @@ router.get(
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['write:attributes']),
|
||||
authorize(['products.attributes']),
|
||||
createValidation,
|
||||
validate,
|
||||
audit('CREATE_ATTRIBUTE_GROUP'),
|
||||
@@ -90,7 +90,7 @@ router.post(
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:attributes']),
|
||||
authorize(['products.attributes']),
|
||||
updateValidation,
|
||||
validate,
|
||||
audit('UPDATE_ATTRIBUTE_GROUP'),
|
||||
@@ -114,7 +114,7 @@ router.put(
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:attributes']),
|
||||
authorize(['products.attributes']),
|
||||
deleteValidation,
|
||||
validate,
|
||||
audit('DELETE_ATTRIBUTE_GROUP'),
|
||||
|
||||
@@ -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 './attributeSet.validation.js';
|
||||
|
||||
const router = Router();
|
||||
@@ -26,7 +26,7 @@ const router = Router();
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['read:attributes']),
|
||||
authorize(['products.attributes']),
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
@@ -47,7 +47,7 @@ router.get(
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['read:attributes']),
|
||||
authorize(['products.attributes']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
controller.getById
|
||||
@@ -56,7 +56,7 @@ router.get(
|
||||
router.get(
|
||||
'/:id/structure',
|
||||
authenticate,
|
||||
authorize(['read:attributes']),
|
||||
authorize(['products.attributes']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
controller.getById
|
||||
@@ -75,7 +75,7 @@ router.get(
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['write:attributes']),
|
||||
authorize(['products.attributes']),
|
||||
createValidation,
|
||||
validate,
|
||||
audit('CREATE_ATTRIBUTE_SET'),
|
||||
@@ -99,7 +99,7 @@ router.post(
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:attributes']),
|
||||
authorize(['products.attributes']),
|
||||
updateValidation,
|
||||
validate,
|
||||
audit('UPDATE_ATTRIBUTE_SET'),
|
||||
@@ -123,7 +123,7 @@ router.put(
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:attributes']),
|
||||
authorize(['products.attributes']),
|
||||
deleteValidation,
|
||||
validate,
|
||||
audit('DELETE_ATTRIBUTE_SET'),
|
||||
|
||||
@@ -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 './attribute.validation.js';
|
||||
|
||||
const router = Router();
|
||||
@@ -26,7 +26,7 @@ const router = Router();
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['read:attributes']),
|
||||
authorize(['products.attributes']),
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@ router.get(
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['read:attributes']),
|
||||
authorize(['products.attributes']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
controller.getById
|
||||
@@ -68,7 +68,7 @@ router.get(
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['write:attributes']),
|
||||
authorize(['products.attributes']),
|
||||
createValidation,
|
||||
validate,
|
||||
audit('CREATE_ATTRIBUTE'),
|
||||
@@ -92,7 +92,7 @@ router.post(
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:attributes']),
|
||||
authorize(['products.attributes']),
|
||||
updateValidation,
|
||||
validate,
|
||||
audit('UPDATE_ATTRIBUTE'),
|
||||
@@ -116,7 +116,7 @@ router.put(
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:attributes']),
|
||||
authorize(['products.attributes']),
|
||||
deleteValidation,
|
||||
validate,
|
||||
audit('DELETE_ATTRIBUTE'),
|
||||
@@ -126,7 +126,7 @@ router.delete(
|
||||
router.post(
|
||||
'/:id/restore',
|
||||
authenticate,
|
||||
authorize(['write:attributes']),
|
||||
authorize(['products.attributes']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
audit('RESTORE_ATTRIBUTE'),
|
||||
|
||||
@@ -2,6 +2,7 @@ import repository from './attribute.repository.js';
|
||||
import { models, sequelize } from '../../../shared/database/models.js';
|
||||
import { SocketService } from '../../../shared/services/socket.service.js';
|
||||
import { AuditService } from '../../../shared/services/audit.service.js';
|
||||
import { ApiError } from '../../../utils/helpers/ApiError.utils.js';
|
||||
import { Op } from 'sequelize';
|
||||
|
||||
export class AttributeService {
|
||||
@@ -122,7 +123,7 @@ export class AttributeService {
|
||||
]
|
||||
});
|
||||
if (!record) {
|
||||
throw new Error('Attribute not found');
|
||||
throw new ApiError(404, 'Attribute not found');
|
||||
}
|
||||
return record;
|
||||
}
|
||||
@@ -306,45 +307,64 @@ export class AttributeService {
|
||||
try {
|
||||
const record = await models.Attribute.findByPk(id, { transaction });
|
||||
if (!record) {
|
||||
throw new Error('Attribute not found');
|
||||
throw new ApiError(404, 'Attribute not found');
|
||||
}
|
||||
|
||||
// Usage Check: Groups mapping
|
||||
const groupCount = await models.AttributeGroupAttribute.count({
|
||||
where: { attribute_id: id },
|
||||
// Usage Check: Groups mapping (count only active groups)
|
||||
const groupCount = await models.AttributeGroup.count({
|
||||
include: [{
|
||||
model: models.Attribute,
|
||||
as: 'attributes',
|
||||
where: { id: id },
|
||||
required: true
|
||||
}],
|
||||
transaction
|
||||
});
|
||||
|
||||
// Usage Check: Product Families mapping (count only active catalogs)
|
||||
const familyCount = await models.Catalog.count({
|
||||
include: [{
|
||||
model: models.Attribute,
|
||||
as: 'attributes',
|
||||
where: { id: id },
|
||||
required: true
|
||||
}],
|
||||
transaction
|
||||
});
|
||||
if (groupCount > 0) {
|
||||
throw new Error('This attribute is currently in use and cannot be deleted.');
|
||||
}
|
||||
|
||||
// Usage Check: Product Families mapping
|
||||
const familyCount = await models.FamilyAttribute.count({
|
||||
where: { attribute_id: id },
|
||||
// Usage Check: Variant axes mapping (count only active catalogs)
|
||||
const axisCount = await models.Catalog.count({
|
||||
include: [{
|
||||
model: models.Attribute,
|
||||
as: 'variantAxes',
|
||||
where: { id: id },
|
||||
required: true
|
||||
}],
|
||||
transaction
|
||||
});
|
||||
if (familyCount > 0) {
|
||||
throw new Error('This attribute is currently in use and cannot be deleted.');
|
||||
}
|
||||
|
||||
// Usage Check: Variant axes mapping
|
||||
const axisCount = await models.FamilyVariantAxis.count({
|
||||
where: { attribute_id: id },
|
||||
transaction
|
||||
});
|
||||
if (axisCount > 0) {
|
||||
throw new Error('This attribute is currently in use and cannot be deleted.');
|
||||
}
|
||||
|
||||
// Usage Check: Variant Values
|
||||
let valCount = 0;
|
||||
if (models.VariantValue) {
|
||||
const valCount = await models.VariantValue.count({
|
||||
valCount = await models.VariantValue.count({
|
||||
where: { axis_id: id },
|
||||
transaction
|
||||
});
|
||||
if (valCount > 0) {
|
||||
throw new Error('This attribute is currently in use and cannot be deleted.');
|
||||
}
|
||||
}
|
||||
|
||||
console.log('DIAGNOSTIC attribute delete:', { id, groupCount, familyCount, axisCount, valCount });
|
||||
|
||||
if (groupCount > 0) {
|
||||
throw new Error('This attribute is currently in use and cannot be deleted.');
|
||||
}
|
||||
if (familyCount > 0) {
|
||||
throw new Error('This attribute is currently in use and cannot be deleted.');
|
||||
}
|
||||
if (axisCount > 0) {
|
||||
throw new Error('This attribute is currently in use and cannot be deleted.');
|
||||
}
|
||||
if (valCount > 0) {
|
||||
throw new Error('This attribute is currently in use and cannot be deleted.');
|
||||
}
|
||||
|
||||
const oldValues = record.toJSON();
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Model, DataTypes } from 'sequelize';
|
||||
|
||||
export class AuditLog extends Model {
|
||||
static associate(models) {
|
||||
// Define associations here
|
||||
// user_id is a plain INTEGER reference - no Sequelize association needed for audit log
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,17 +14,37 @@ export default (sequelize) => {
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
name: {
|
||||
action: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
resource: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
resource_id: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
},
|
||||
old_value: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true
|
||||
},
|
||||
new_value: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true
|
||||
},
|
||||
details: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: 'active'
|
||||
},
|
||||
metadata: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true
|
||||
defaultValue: 'success'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
|
||||
@@ -26,7 +26,7 @@ const router = Router();
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['read:auditLogs']),
|
||||
authorize(['settings.roles']),
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@ router.get(
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['read:auditLogs']),
|
||||
authorize(['settings.roles']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
controller.getById
|
||||
@@ -68,7 +68,7 @@ router.get(
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['write:auditLogs']),
|
||||
authorize(['settings.roles']),
|
||||
createValidation,
|
||||
validate,
|
||||
audit('CREATE_AUDITLOG'),
|
||||
@@ -92,7 +92,7 @@ router.post(
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:auditLogs']),
|
||||
authorize(['settings.roles']),
|
||||
updateValidation,
|
||||
validate,
|
||||
audit('UPDATE_AUDITLOG'),
|
||||
@@ -116,7 +116,7 @@ router.put(
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:auditLogs']),
|
||||
authorize(['settings.roles']),
|
||||
deleteValidation,
|
||||
validate,
|
||||
audit('DELETE_AUDITLOG'),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import repository from './auditLog.repository.js';
|
||||
import { SocketService } from '../../../shared/services/socket.service.js';
|
||||
import { AuditService } from '../../../shared/services/audit.service.js';
|
||||
import { ApiError } from '../../../utils/helpers/ApiError.utils.js';
|
||||
|
||||
export class AuditLogService {
|
||||
async getAll(query = {}) {
|
||||
@@ -11,7 +12,7 @@ export class AuditLogService {
|
||||
async getById(id) {
|
||||
const record = await repository.findById(id);
|
||||
if (!record) {
|
||||
throw new Error('AuditLog not found');
|
||||
throw new ApiError(404, 'AuditLog not found');
|
||||
}
|
||||
return record;
|
||||
}
|
||||
@@ -27,7 +28,7 @@ export class AuditLogService {
|
||||
action: 'CREATE',
|
||||
resource: 'AuditLog',
|
||||
resourceId: record.id,
|
||||
userId: userContext.id || 'system',
|
||||
userId: userContext.userId || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
@@ -37,7 +38,7 @@ export class AuditLogService {
|
||||
async update(id, data, userContext = {}) {
|
||||
const record = await repository.update(id, data);
|
||||
if (!record) {
|
||||
throw new Error('AuditLog not found');
|
||||
throw new ApiError(404, 'AuditLog not found');
|
||||
}
|
||||
|
||||
SocketService.broadcast('auditLog:updated', record);
|
||||
@@ -46,7 +47,7 @@ export class AuditLogService {
|
||||
action: 'UPDATE',
|
||||
resource: 'AuditLog',
|
||||
resourceId: id,
|
||||
userId: userContext.id || 'system',
|
||||
userId: userContext.userId || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
@@ -56,7 +57,7 @@ export class AuditLogService {
|
||||
async delete(id, userContext = {}) {
|
||||
const deleted = await repository.delete(id);
|
||||
if (!deleted) {
|
||||
throw new Error('AuditLog not found');
|
||||
throw new ApiError(404, 'AuditLog not found');
|
||||
}
|
||||
|
||||
SocketService.broadcast('auditLog:deleted', { id });
|
||||
@@ -65,7 +66,7 @@ export class AuditLogService {
|
||||
action: 'DELETE',
|
||||
resource: 'AuditLog',
|
||||
resourceId: id,
|
||||
userId: userContext.id || 'system'
|
||||
userId: userContext.userId || 'system'
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
@@ -22,7 +22,7 @@ const router = Router();
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['settings.roles.view']),
|
||||
authorize(['settings.roles']),
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
@@ -66,7 +66,7 @@ router.get(
|
||||
router.get(
|
||||
'/permissions',
|
||||
authenticate,
|
||||
authorize(['settings.roles.view']),
|
||||
authorize(['settings.roles']),
|
||||
controller.getPermissions
|
||||
);
|
||||
|
||||
@@ -93,7 +93,7 @@ router.get(
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['settings.roles.view']),
|
||||
authorize(['settings.roles']),
|
||||
controller.getById
|
||||
);
|
||||
|
||||
@@ -126,7 +126,7 @@ router.get(
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['settings.roles.create']),
|
||||
authorize(['settings.roles']),
|
||||
controller.create
|
||||
);
|
||||
|
||||
@@ -151,7 +151,7 @@ router.post(
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['settings.roles.edit']),
|
||||
authorize(['settings.roles']),
|
||||
controller.update
|
||||
);
|
||||
|
||||
@@ -176,7 +176,7 @@ router.put(
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['settings.roles.delete']),
|
||||
authorize(['settings.roles']),
|
||||
controller.delete
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import repository from './role.repository.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
import { ApiError } from '../../../utils/helpers/ApiError.utils.js';
|
||||
|
||||
export class RoleService {
|
||||
async getAll(query = {}, context = {}) {
|
||||
@@ -20,7 +21,7 @@ export class RoleService {
|
||||
async getById(id, context = {}) {
|
||||
const record = await repository.findById(id, {}, context);
|
||||
if (!record) {
|
||||
throw new Error('Role not found');
|
||||
throw new ApiError(404, 'Role not found');
|
||||
}
|
||||
return record;
|
||||
}
|
||||
@@ -61,14 +62,18 @@ export class RoleService {
|
||||
async delete(id, context = {}) {
|
||||
const deleted = await repository.delete(id, context);
|
||||
if (!deleted) {
|
||||
throw new Error('Role not found');
|
||||
throw new ApiError(404, 'Role not found');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get all permission nodes currently in database
|
||||
// Get all permission nodes — returns full rows including capability flags
|
||||
// (can_view, can_create, can_edit, can_delete, can_alter, can_export, can_import)
|
||||
// The frontend uses these flags to determine which action pills to render per module.
|
||||
async getPermissionNodes(context = {}) {
|
||||
return await models.PermissionNode.findAll();
|
||||
return await models.PermissionNode.findAll({
|
||||
order: [['display_order', 'ASC'], ['node_level', 'ASC'], ['id', 'ASC']],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ const router = Router();
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['settings.users.view']),
|
||||
authorize(['settings.users']),
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@ router.get(
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['settings.users.view']),
|
||||
authorize(['settings.users']),
|
||||
controller.getById
|
||||
);
|
||||
|
||||
@@ -82,7 +82,7 @@ router.get(
|
||||
router.post(
|
||||
'/invite',
|
||||
authenticate,
|
||||
authorize(['settings.users.create']),
|
||||
authorize(['settings.users']),
|
||||
controller.invite
|
||||
);
|
||||
|
||||
@@ -107,7 +107,7 @@ router.post(
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['settings.users.edit']),
|
||||
authorize(['settings.users']),
|
||||
controller.update
|
||||
);
|
||||
|
||||
@@ -132,7 +132,7 @@ router.put(
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['settings.users.delete']),
|
||||
authorize(['settings.users']),
|
||||
controller.delete
|
||||
);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import repository from './user.repository.js';
|
||||
import { sendInvitationEmail } from '../../../shared/utils/email.js';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
import { ApiError } from '../../../utils/helpers/ApiError.utils.js';
|
||||
|
||||
export class UserService {
|
||||
async getAll(query = {}, context = {}) {
|
||||
@@ -11,7 +12,7 @@ export class UserService {
|
||||
async getById(id, context = {}) {
|
||||
const record = await repository.findById(id, {}, context);
|
||||
if (!record) {
|
||||
throw new Error('User not found');
|
||||
throw new ApiError(404, 'User not found');
|
||||
}
|
||||
return record;
|
||||
}
|
||||
@@ -22,7 +23,7 @@ export class UserService {
|
||||
// Check if user already exists
|
||||
const existingUser = await repository.findByEmail(email);
|
||||
if (existingUser) {
|
||||
throw new Error('User with this email already exists');
|
||||
throw new ApiError(409, 'User with this email already exists');
|
||||
}
|
||||
|
||||
// Generate temporary password
|
||||
@@ -83,7 +84,7 @@ export class UserService {
|
||||
async delete(id, context = {}) {
|
||||
const deleted = await repository.delete(id, context);
|
||||
if (!deleted) {
|
||||
throw new Error('User not found');
|
||||
throw new ApiError(404, 'User not found');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -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 './brand.validation.js';
|
||||
|
||||
const router = Router();
|
||||
@@ -26,7 +26,7 @@ const router = Router();
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['read:brands']),
|
||||
authorize(['masters.brands']),
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@ router.get(
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['read:brands']),
|
||||
authorize(['masters.brands']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
controller.getById
|
||||
@@ -68,7 +68,7 @@ router.get(
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['write:brands']),
|
||||
authorize(['masters.brands']),
|
||||
createValidation,
|
||||
validate,
|
||||
audit('CREATE_BRAND'),
|
||||
@@ -92,7 +92,7 @@ router.post(
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:brands']),
|
||||
authorize(['masters.brands']),
|
||||
updateValidation,
|
||||
validate,
|
||||
audit('UPDATE_BRAND'),
|
||||
@@ -116,7 +116,7 @@ router.put(
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:brands']),
|
||||
authorize(['masters.brands']),
|
||||
deleteValidation,
|
||||
validate,
|
||||
audit('DELETE_BRAND'),
|
||||
@@ -126,7 +126,7 @@ router.delete(
|
||||
router.post(
|
||||
'/:id/archive',
|
||||
authenticate,
|
||||
authorize(['write:brands']),
|
||||
authorize(['masters.brands']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
audit('ARCHIVE_BRAND'),
|
||||
@@ -136,7 +136,7 @@ router.post(
|
||||
router.post(
|
||||
'/:id/restore',
|
||||
authenticate,
|
||||
authorize(['write:brands']),
|
||||
authorize(['masters.brands']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
audit('RESTORE_BRAND'),
|
||||
|
||||
@@ -2,6 +2,8 @@ import repository from './brand.repository.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
import { SocketService } from '../../../shared/services/socket.service.js';
|
||||
import { AuditService } from '../../../shared/services/audit.service.js';
|
||||
import NotificationService from '../../notifications/notifications/notification.service.js';
|
||||
import { ApiError } from '../../../utils/helpers/ApiError.utils.js';
|
||||
|
||||
export class BrandService {
|
||||
async getAll(query = {}, context = {}) {
|
||||
@@ -15,7 +17,7 @@ export class BrandService {
|
||||
async getById(id, context = {}) {
|
||||
const record = await repository.findById(id, {}, context);
|
||||
if (!record) {
|
||||
throw new Error('Brand not found');
|
||||
throw new ApiError(404, 'Brand not found');
|
||||
}
|
||||
return record;
|
||||
}
|
||||
@@ -47,17 +49,29 @@ export class BrandService {
|
||||
action: 'CREATE',
|
||||
resource: 'Brand',
|
||||
resourceId: record.id,
|
||||
userId: context.userId || 'system',
|
||||
userId: userContext.userId || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
// Notify tenant users
|
||||
if (userContext.tenantId) {
|
||||
NotificationService.notifyTenant(userContext.tenantId, userContext.userId, {
|
||||
variant: 'brand',
|
||||
action: 'created',
|
||||
title: 'New brand added',
|
||||
description: `Brand "${record.name || 'A brand'}" was registered in the master data.`,
|
||||
entity: record.name || 'Brand',
|
||||
entity_id: record.id.toString()
|
||||
}).catch(err => console.error('Notification failed:', err));
|
||||
}
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
async update(id, data, context = {}) {
|
||||
const record = await repository.findById(id, {}, context);
|
||||
if (!record) {
|
||||
throw new Error('Brand not found');
|
||||
throw new ApiError(404, 'Brand not found');
|
||||
}
|
||||
|
||||
if (data.code && data.code !== record.code) {
|
||||
@@ -75,17 +89,29 @@ export class BrandService {
|
||||
action: 'UPDATE',
|
||||
resource: 'Brand',
|
||||
resourceId: id,
|
||||
userId: context.userId || 'system',
|
||||
userId: userContext.userId || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
// Notify tenant users
|
||||
if (userContext.tenantId) {
|
||||
NotificationService.notifyTenant(userContext.tenantId, userContext.userId, {
|
||||
variant: 'brand',
|
||||
action: 'updated',
|
||||
title: 'Brand updated',
|
||||
description: `"${record.name || 'A brand'}" brand details were refreshed.`,
|
||||
entity: record.name || 'Brand',
|
||||
entity_id: id.toString()
|
||||
}).catch(err => console.error('Notification failed:', err));
|
||||
}
|
||||
|
||||
return updatedRecord;
|
||||
}
|
||||
|
||||
async delete(id, context = {}) {
|
||||
const record = await repository.findById(id, {}, context);
|
||||
if (!record) {
|
||||
throw new Error('Brand not found');
|
||||
throw new ApiError(404, 'Brand not found');
|
||||
}
|
||||
|
||||
// Check product linkage
|
||||
@@ -103,9 +129,21 @@ export class BrandService {
|
||||
action: 'DELETE',
|
||||
resource: 'Brand',
|
||||
resourceId: id,
|
||||
userId: context.userId || 'system'
|
||||
userId: userContext.userId || userContext.id || 'system'
|
||||
});
|
||||
|
||||
// Notify tenant users
|
||||
if (userContext.tenantId) {
|
||||
NotificationService.notifyTenant(userContext.tenantId, userContext.userId, {
|
||||
variant: 'brand',
|
||||
action: 'deleted',
|
||||
title: 'Brand deleted',
|
||||
description: 'A brand was permanently removed from the master data.',
|
||||
entity: 'Brand',
|
||||
entity_id: id.toString()
|
||||
}).catch(err => console.error('Notification failed:', err));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -124,9 +162,21 @@ export class BrandService {
|
||||
action: 'ARCHIVE',
|
||||
resource: 'Brand',
|
||||
resourceId: id,
|
||||
userId: context.userId || 'system'
|
||||
userId: userContext.userId || userContext.id || 'system'
|
||||
});
|
||||
|
||||
// Notify tenant users
|
||||
if (userContext.tenantId) {
|
||||
NotificationService.notifyTenant(userContext.tenantId, userContext.userId, {
|
||||
variant: 'brand',
|
||||
action: 'deleted',
|
||||
title: 'Brand deleted',
|
||||
description: 'A brand was permanently removed from the master data.',
|
||||
entity: 'Brand',
|
||||
entity_id: id.toString()
|
||||
}).catch(err => console.error('Notification failed:', err));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 './unit.validation.js';
|
||||
|
||||
const router = Router();
|
||||
@@ -26,7 +26,7 @@ const router = Router();
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['read:brands']),
|
||||
authorize(['masters.units']),
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
@@ -47,7 +47,7 @@ router.get(
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['read:brands']),
|
||||
authorize(['masters.units']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
controller.getById
|
||||
@@ -66,7 +66,7 @@ router.get(
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['write:brands']),
|
||||
authorize(['masters.units']),
|
||||
createValidation,
|
||||
validate,
|
||||
audit('CREATE_UNIT'),
|
||||
@@ -90,7 +90,7 @@ router.post(
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:brands']),
|
||||
authorize(['masters.units']),
|
||||
updateValidation,
|
||||
validate,
|
||||
audit('UPDATE_UNIT'),
|
||||
@@ -114,7 +114,7 @@ router.put(
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:brands']),
|
||||
authorize(['masters.units']),
|
||||
deleteValidation,
|
||||
validate,
|
||||
audit('DELETE_UNIT'),
|
||||
@@ -124,7 +124,7 @@ router.delete(
|
||||
router.post(
|
||||
'/:id/archive',
|
||||
authenticate,
|
||||
authorize(['write:brands']),
|
||||
authorize(['masters.units']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
audit('ARCHIVE_UNIT'),
|
||||
@@ -134,7 +134,7 @@ router.post(
|
||||
router.post(
|
||||
'/:id/restore',
|
||||
authenticate,
|
||||
authorize(['write:brands']),
|
||||
authorize(['masters.units']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
audit('RESTORE_UNIT'),
|
||||
|
||||
@@ -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 './catalog.validation.js';
|
||||
|
||||
const router = Router();
|
||||
@@ -26,7 +26,7 @@ const router = Router();
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['read:catalogs']),
|
||||
authorize(['products.families']),
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@ router.get(
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['read:catalogs']),
|
||||
authorize(['products.families']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
controller.getById
|
||||
@@ -58,7 +58,7 @@ router.get(
|
||||
router.get(
|
||||
'/:id/blueprint',
|
||||
authenticate,
|
||||
authorize(['read:catalogs']),
|
||||
authorize(['products.families']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
controller.getBlueprint
|
||||
@@ -67,7 +67,7 @@ router.get(
|
||||
router.get(
|
||||
'/:id/summary',
|
||||
authenticate,
|
||||
authorize(['read:catalogs']),
|
||||
authorize(['products.families']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
controller.getSummary
|
||||
@@ -86,7 +86,7 @@ router.get(
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['write:catalogs']),
|
||||
authorize(['products.families']),
|
||||
createValidation,
|
||||
validate,
|
||||
audit('CREATE_CATALOG'),
|
||||
@@ -110,7 +110,7 @@ router.post(
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:catalogs']),
|
||||
authorize(['products.families']),
|
||||
updateValidation,
|
||||
validate,
|
||||
audit('UPDATE_CATALOG'),
|
||||
@@ -134,7 +134,7 @@ router.put(
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:catalogs']),
|
||||
authorize(['products.families']),
|
||||
deleteValidation,
|
||||
validate,
|
||||
audit('DELETE_CATALOG'),
|
||||
@@ -158,7 +158,7 @@ router.delete(
|
||||
router.post(
|
||||
'/:id/restore',
|
||||
authenticate,
|
||||
authorize(['write:catalogs']),
|
||||
authorize(['products.families']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
audit('RESTORE_CATALOG'),
|
||||
|
||||
@@ -3,6 +3,7 @@ import { models, sequelize } from '../../../shared/database/models.js';
|
||||
import { SocketService } from '../../../shared/services/socket.service.js';
|
||||
import { AuditService } from '../../../shared/services/audit.service.js';
|
||||
import { ApiError } from '../../../utils/helpers/ApiError.utils.js';
|
||||
import { ApiError } from '../../../utils/helpers/ApiError.utils.js';
|
||||
|
||||
export class CatalogService {
|
||||
async attachCounts(record, transaction) {
|
||||
@@ -120,7 +121,7 @@ export class CatalogService {
|
||||
async getById(id, context = {}) {
|
||||
const record = await repository.findById(id, {}, context);
|
||||
if (!record) {
|
||||
throw new Error('Product Family not found');
|
||||
throw new ApiError(404, 'Product Family not found');
|
||||
}
|
||||
await this.attachCounts(record);
|
||||
return record;
|
||||
@@ -361,7 +362,7 @@ export class CatalogService {
|
||||
action: 'CREATE',
|
||||
resource: 'Catalog',
|
||||
resourceId: record.id,
|
||||
userId: context.userId || 'system',
|
||||
userId: userContext.userId || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
@@ -675,7 +676,7 @@ export class CatalogService {
|
||||
action: 'DELETE',
|
||||
resource: 'Catalog',
|
||||
resourceId: id,
|
||||
userId: context.userId || 'system'
|
||||
userId: userContext.userId || 'system'
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
@@ -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 './categorie.validation.js';
|
||||
|
||||
const router = Router();
|
||||
@@ -26,7 +26,7 @@ const router = Router();
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['read:categories']),
|
||||
authorize(['products.categories']),
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@ router.get(
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['read:categories']),
|
||||
authorize(['products.categories']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
controller.getById
|
||||
@@ -68,7 +68,7 @@ router.get(
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['write:categories']),
|
||||
authorize(['products.categories']),
|
||||
createValidation,
|
||||
validate,
|
||||
audit('CREATE_CATEGORIE'),
|
||||
@@ -92,7 +92,7 @@ router.post(
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:categories']),
|
||||
authorize(['products.categories']),
|
||||
updateValidation,
|
||||
validate,
|
||||
audit('UPDATE_CATEGORIE'),
|
||||
@@ -116,7 +116,7 @@ router.put(
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:categories']),
|
||||
authorize(['products.categories']),
|
||||
deleteValidation,
|
||||
validate,
|
||||
audit('DELETE_CATEGORIE'),
|
||||
@@ -126,7 +126,7 @@ router.delete(
|
||||
router.post(
|
||||
'/:id/archive',
|
||||
authenticate,
|
||||
authorize(['write:categories']),
|
||||
authorize(['products.categories']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
audit('ARCHIVE_CATEGORIE'),
|
||||
@@ -136,7 +136,7 @@ router.post(
|
||||
router.post(
|
||||
'/:id/restore',
|
||||
authenticate,
|
||||
authorize(['write:categories']),
|
||||
authorize(['products.categories']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
audit('RESTORE_CATEGORIE'),
|
||||
|
||||
@@ -2,6 +2,7 @@ import repository from './categorie.repository.js';
|
||||
import { models, sequelize } from '../../../shared/database/models.js';
|
||||
import { SocketService } from '../../../shared/services/socket.service.js';
|
||||
import { AuditService } from '../../../shared/services/audit.service.js';
|
||||
import NotificationService from '../../notifications/notifications/notification.service.js';
|
||||
import { ApiError } from '../../../utils/helpers/ApiError.utils.js';
|
||||
import { Op } from 'sequelize';
|
||||
|
||||
@@ -17,7 +18,7 @@ export class CategorieService {
|
||||
async getById(id, context = {}) {
|
||||
const record = await repository.findById(id, {}, context);
|
||||
if (!record) {
|
||||
throw new ApiError(404, 'Category not found');
|
||||
throw new ApiApiError(404, 404, 'Category not found');
|
||||
}
|
||||
return record;
|
||||
}
|
||||
@@ -197,6 +198,18 @@ export class CategorieService {
|
||||
details: data
|
||||
});
|
||||
|
||||
// Notify tenant users
|
||||
if (userContext.tenantId) {
|
||||
NotificationService.notifyTenant(userContext.tenantId, userContext.userId, {
|
||||
variant: 'category',
|
||||
action: 'updated',
|
||||
title: 'Category updated',
|
||||
description: `Category "${record.name || 'A category'}" tree details were updated.`,
|
||||
entity: record.name || 'Category',
|
||||
entity_id: id.toString()
|
||||
}).catch(err => console.error('Notification failed:', err));
|
||||
}
|
||||
|
||||
return fullRecord;
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
@@ -249,9 +262,21 @@ export class CategorieService {
|
||||
action: 'DELETE',
|
||||
resource: 'Categorie',
|
||||
resourceId: id,
|
||||
userId: context.userId || 'system'
|
||||
userId: userContext.userId || 'system'
|
||||
});
|
||||
|
||||
// Notify tenant users
|
||||
if (userContext.tenantId) {
|
||||
NotificationService.notifyTenant(userContext.tenantId, userContext.userId, {
|
||||
variant: 'category',
|
||||
action: 'deleted',
|
||||
title: 'Category deleted',
|
||||
description: 'A category was permanently removed from taxonomy.',
|
||||
entity: 'Category',
|
||||
entity_id: id.toString()
|
||||
}).catch(err => console.error('Notification failed:', err));
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
@@ -278,9 +303,21 @@ export class CategorieService {
|
||||
action: 'ARCHIVE',
|
||||
resource: 'Categorie',
|
||||
resourceId: id,
|
||||
userId: context.userId || 'system'
|
||||
userId: userContext.userId || 'system'
|
||||
});
|
||||
|
||||
// Notify tenant users
|
||||
if (userContext.tenantId) {
|
||||
NotificationService.notifyTenant(userContext.tenantId, userContext.userId, {
|
||||
variant: 'category',
|
||||
action: 'deleted',
|
||||
title: 'Category deleted',
|
||||
description: 'A category was permanently removed from taxonomy.',
|
||||
entity: 'Category',
|
||||
entity_id: id.toString()
|
||||
}).catch(err => console.error('Notification failed:', err));
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
@@ -305,7 +342,7 @@ export class CategorieService {
|
||||
action: 'RESTORE',
|
||||
resource: 'Categorie',
|
||||
resourceId: id,
|
||||
userId: context.userId || 'system'
|
||||
userId: userContext.userId || 'system'
|
||||
});
|
||||
|
||||
return restored;
|
||||
|
||||
@@ -16,14 +16,14 @@ const router = Router();
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['read:channels']),
|
||||
authorize(['settings.integrations']),
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['read:channels']),
|
||||
authorize(['settings.integrations']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
controller.getById
|
||||
@@ -32,7 +32,7 @@ router.get(
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['write:channels']),
|
||||
authorize(['settings.integrations']),
|
||||
createValidation,
|
||||
validate,
|
||||
audit('CREATE_CHANNEL_TYPE'),
|
||||
@@ -42,7 +42,7 @@ router.post(
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:channels']),
|
||||
authorize(['settings.integrations']),
|
||||
updateValidation,
|
||||
validate,
|
||||
audit('UPDATE_CHANNEL_TYPE'),
|
||||
@@ -52,7 +52,7 @@ router.put(
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:channels']),
|
||||
authorize(['settings.integrations']),
|
||||
deleteValidation,
|
||||
validate,
|
||||
audit('DELETE_CHANNEL_TYPE'),
|
||||
@@ -62,7 +62,7 @@ router.delete(
|
||||
router.post(
|
||||
'/:id/archive',
|
||||
authenticate,
|
||||
authorize(['write:channels']),
|
||||
authorize(['settings.integrations']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
audit('ARCHIVE_CHANNEL_TYPE'),
|
||||
@@ -72,7 +72,7 @@ router.post(
|
||||
router.post(
|
||||
'/:id/restore',
|
||||
authenticate,
|
||||
authorize(['write:channels']),
|
||||
authorize(['settings.integrations']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
audit('RESTORE_CHANNEL_TYPE'),
|
||||
|
||||
@@ -26,7 +26,7 @@ const router = Router();
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['read:channels']),
|
||||
authorize(['settings.integrations']),
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@ router.get(
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['read:channels']),
|
||||
authorize(['settings.integrations']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
controller.getById
|
||||
@@ -68,7 +68,7 @@ router.get(
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['write:channels']),
|
||||
authorize(['settings.integrations']),
|
||||
createValidation,
|
||||
validate,
|
||||
audit('CREATE_CHANNEL'),
|
||||
@@ -92,7 +92,7 @@ router.post(
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:channels']),
|
||||
authorize(['settings.integrations']),
|
||||
updateValidation,
|
||||
validate,
|
||||
audit('UPDATE_CHANNEL'),
|
||||
@@ -116,7 +116,7 @@ router.put(
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:channels']),
|
||||
authorize(['settings.integrations']),
|
||||
deleteValidation,
|
||||
validate,
|
||||
audit('DELETE_CHANNEL'),
|
||||
@@ -126,7 +126,7 @@ router.delete(
|
||||
router.post(
|
||||
'/:id/archive',
|
||||
authenticate,
|
||||
authorize(['write:channels']),
|
||||
authorize(['settings.integrations']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
audit('ARCHIVE_CHANNEL'),
|
||||
@@ -136,7 +136,7 @@ router.post(
|
||||
router.post(
|
||||
'/:id/restore',
|
||||
authenticate,
|
||||
authorize(['write:channels']),
|
||||
authorize(['settings.integrations']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
audit('RESTORE_CHANNEL'),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import repository from './channel.repository.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
import { SocketService } from '../../../shared/services/socket.service.js';
|
||||
import { AuditService } from '../../../shared/services/audit.service.js';
|
||||
import { ApiError } from '../../../utils/helpers/ApiError.utils.js';
|
||||
|
||||
export class ChannelService {
|
||||
async getAll(query = {}) {
|
||||
@@ -11,7 +13,7 @@ export class ChannelService {
|
||||
async getById(id) {
|
||||
const record = await repository.findById(id);
|
||||
if (!record) {
|
||||
throw new Error('Channel not found');
|
||||
throw new ApiError(404, 'Channel not found');
|
||||
}
|
||||
return record;
|
||||
}
|
||||
@@ -27,7 +29,7 @@ export class ChannelService {
|
||||
action: 'CREATE',
|
||||
resource: 'Channel',
|
||||
resourceId: record.id,
|
||||
userId: userContext.id || 'system',
|
||||
userId: userContext.userId || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
@@ -37,7 +39,7 @@ export class ChannelService {
|
||||
async update(id, data, userContext = {}) {
|
||||
const record = await repository.update(id, data);
|
||||
if (!record) {
|
||||
throw new Error('Channel not found');
|
||||
throw new ApiError(404, 'Channel not found');
|
||||
}
|
||||
|
||||
SocketService.broadcast('channel:updated', record);
|
||||
@@ -46,7 +48,7 @@ export class ChannelService {
|
||||
action: 'UPDATE',
|
||||
resource: 'Channel',
|
||||
resourceId: id,
|
||||
userId: userContext.id || 'system',
|
||||
userId: userContext.userId || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
@@ -56,7 +58,7 @@ export class ChannelService {
|
||||
async delete(id, userContext = {}) {
|
||||
const record = await models.Channel.findByPk(id);
|
||||
if (!record) {
|
||||
throw new Error('Channel not found');
|
||||
throw new ApiError(404, 'Channel not found');
|
||||
}
|
||||
|
||||
// Check usage in families
|
||||
@@ -84,7 +86,7 @@ export class ChannelService {
|
||||
action: 'DELETE',
|
||||
resource: 'Channel',
|
||||
resourceId: id,
|
||||
userId: userContext.id || 'system'
|
||||
userId: userContext.userId || 'system'
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
@@ -26,7 +26,7 @@ const router = Router();
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['read:metrics']),
|
||||
authorize(['reports.sales']),
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@ router.get(
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['read:metrics']),
|
||||
authorize(['reports.sales']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
controller.getById
|
||||
@@ -68,7 +68,7 @@ router.get(
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['write:metrics']),
|
||||
authorize(['reports.sales']),
|
||||
createValidation,
|
||||
validate,
|
||||
audit('CREATE_METRIC'),
|
||||
@@ -92,7 +92,7 @@ router.post(
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:metrics']),
|
||||
authorize(['reports.sales']),
|
||||
updateValidation,
|
||||
validate,
|
||||
audit('UPDATE_METRIC'),
|
||||
@@ -116,7 +116,7 @@ router.put(
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:metrics']),
|
||||
authorize(['reports.sales']),
|
||||
deleteValidation,
|
||||
validate,
|
||||
audit('DELETE_METRIC'),
|
||||
|
||||
@@ -27,7 +27,7 @@ export class MetricService {
|
||||
action: 'CREATE',
|
||||
resource: 'Metric',
|
||||
resourceId: record.id,
|
||||
userId: userContext.id || 'system',
|
||||
userId: userContext.userId || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
@@ -46,7 +46,7 @@ export class MetricService {
|
||||
action: 'UPDATE',
|
||||
resource: 'Metric',
|
||||
resourceId: id,
|
||||
userId: userContext.id || 'system',
|
||||
userId: userContext.userId || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
@@ -65,7 +65,7 @@ export class MetricService {
|
||||
action: 'DELETE',
|
||||
resource: 'Metric',
|
||||
resourceId: id,
|
||||
userId: userContext.id || 'system'
|
||||
userId: userContext.userId || 'system'
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
@@ -26,7 +26,7 @@ const router = Router();
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['read:exports']),
|
||||
authorize(['settings.integrations']),
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@ router.get(
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['read:exports']),
|
||||
authorize(['settings.integrations']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
controller.getById
|
||||
@@ -68,7 +68,7 @@ router.get(
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['write:exports']),
|
||||
authorize(['settings.integrations']),
|
||||
createValidation,
|
||||
validate,
|
||||
audit('CREATE_EXPORT'),
|
||||
@@ -92,7 +92,7 @@ router.post(
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:exports']),
|
||||
authorize(['settings.integrations']),
|
||||
updateValidation,
|
||||
validate,
|
||||
audit('UPDATE_EXPORT'),
|
||||
@@ -116,7 +116,7 @@ router.put(
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:exports']),
|
||||
authorize(['settings.integrations']),
|
||||
deleteValidation,
|
||||
validate,
|
||||
audit('DELETE_EXPORT'),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import repository from './export.repository.js';
|
||||
import { SocketService } from '../../../shared/services/socket.service.js';
|
||||
import { AuditService } from '../../../shared/services/audit.service.js';
|
||||
import { ApiError } from '../../../utils/helpers/ApiError.utils.js';
|
||||
|
||||
export class ExportService {
|
||||
async getAll(query = {}) {
|
||||
@@ -11,7 +12,7 @@ export class ExportService {
|
||||
async getById(id) {
|
||||
const record = await repository.findById(id);
|
||||
if (!record) {
|
||||
throw new Error('Export not found');
|
||||
throw new ApiError(404, 'Export not found');
|
||||
}
|
||||
return record;
|
||||
}
|
||||
@@ -27,7 +28,7 @@ export class ExportService {
|
||||
action: 'CREATE',
|
||||
resource: 'Export',
|
||||
resourceId: record.id,
|
||||
userId: userContext.id || 'system',
|
||||
userId: userContext.userId || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
@@ -37,7 +38,7 @@ export class ExportService {
|
||||
async update(id, data, userContext = {}) {
|
||||
const record = await repository.update(id, data);
|
||||
if (!record) {
|
||||
throw new Error('Export not found');
|
||||
throw new ApiError(404, 'Export not found');
|
||||
}
|
||||
|
||||
SocketService.broadcast('export:updated', record);
|
||||
@@ -46,7 +47,7 @@ export class ExportService {
|
||||
action: 'UPDATE',
|
||||
resource: 'Export',
|
||||
resourceId: id,
|
||||
userId: userContext.id || 'system',
|
||||
userId: userContext.userId || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
@@ -56,7 +57,7 @@ export class ExportService {
|
||||
async delete(id, userContext = {}) {
|
||||
const deleted = await repository.delete(id);
|
||||
if (!deleted) {
|
||||
throw new Error('Export not found');
|
||||
throw new ApiError(404, 'Export not found');
|
||||
}
|
||||
|
||||
SocketService.broadcast('export:deleted', { id });
|
||||
@@ -65,7 +66,7 @@ export class ExportService {
|
||||
action: 'DELETE',
|
||||
resource: 'Export',
|
||||
resourceId: id,
|
||||
userId: userContext.id || 'system'
|
||||
userId: userContext.userId || 'system'
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
@@ -26,7 +26,7 @@ const router = Router();
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['read:imports']),
|
||||
authorize(['settings.integrations']),
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@ router.get(
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['read:imports']),
|
||||
authorize(['settings.integrations']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
controller.getById
|
||||
@@ -68,7 +68,7 @@ router.get(
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['write:imports']),
|
||||
authorize(['settings.integrations']),
|
||||
createValidation,
|
||||
validate,
|
||||
audit('CREATE_IMPORT'),
|
||||
@@ -92,7 +92,7 @@ router.post(
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:imports']),
|
||||
authorize(['settings.integrations']),
|
||||
updateValidation,
|
||||
validate,
|
||||
audit('UPDATE_IMPORT'),
|
||||
@@ -116,7 +116,7 @@ router.put(
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:imports']),
|
||||
authorize(['settings.integrations']),
|
||||
deleteValidation,
|
||||
validate,
|
||||
audit('DELETE_IMPORT'),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import repository from './import.repository.js';
|
||||
import { SocketService } from '../../../shared/services/socket.service.js';
|
||||
import { AuditService } from '../../../shared/services/audit.service.js';
|
||||
import { ApiError } from '../../../utils/helpers/ApiError.utils.js';
|
||||
|
||||
export class ImportService {
|
||||
async getAll(query = {}) {
|
||||
@@ -11,7 +12,7 @@ export class ImportService {
|
||||
async getById(id) {
|
||||
const record = await repository.findById(id);
|
||||
if (!record) {
|
||||
throw new Error('Import not found');
|
||||
throw new ApiError(404, 'Import not found');
|
||||
}
|
||||
return record;
|
||||
}
|
||||
@@ -27,7 +28,7 @@ export class ImportService {
|
||||
action: 'CREATE',
|
||||
resource: 'Import',
|
||||
resourceId: record.id,
|
||||
userId: userContext.id || 'system',
|
||||
userId: userContext.userId || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
@@ -37,7 +38,7 @@ export class ImportService {
|
||||
async update(id, data, userContext = {}) {
|
||||
const record = await repository.update(id, data);
|
||||
if (!record) {
|
||||
throw new Error('Import not found');
|
||||
throw new ApiError(404, 'Import not found');
|
||||
}
|
||||
|
||||
SocketService.broadcast('import:updated', record);
|
||||
@@ -46,7 +47,7 @@ export class ImportService {
|
||||
action: 'UPDATE',
|
||||
resource: 'Import',
|
||||
resourceId: id,
|
||||
userId: userContext.id || 'system',
|
||||
userId: userContext.userId || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
@@ -56,7 +57,7 @@ export class ImportService {
|
||||
async delete(id, userContext = {}) {
|
||||
const deleted = await repository.delete(id);
|
||||
if (!deleted) {
|
||||
throw new Error('Import not found');
|
||||
throw new ApiError(404, 'Import not found');
|
||||
}
|
||||
|
||||
SocketService.broadcast('import:deleted', { id });
|
||||
@@ -65,7 +66,7 @@ export class ImportService {
|
||||
action: 'DELETE',
|
||||
resource: 'Import',
|
||||
resourceId: id,
|
||||
userId: userContext.id || 'system'
|
||||
userId: userContext.userId || 'system'
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
@@ -12,6 +12,7 @@ import importsRouter from './imports/index.js';
|
||||
import exportsRouter from './exports/index.js';
|
||||
import settingsRouter from './settings/index.js';
|
||||
import auditLogsRouter from './auditLogs/index.js';
|
||||
import notificationsRouter from './notifications/index.js';
|
||||
import workflowsRouter from './workflows/workflow.routes.js';
|
||||
import variantsRouter from './variants/index.js';
|
||||
|
||||
@@ -30,6 +31,7 @@ export default function registerRoutes(app) {
|
||||
app.use('/api/v1', exportsRouter);
|
||||
app.use('/api/v1', settingsRouter);
|
||||
app.use('/api/v1', auditLogsRouter);
|
||||
app.use('/api/v1', notificationsRouter);
|
||||
app.use('/api/v1/workflows', workflowsRouter);
|
||||
app.use('/api/v1', variantsRouter);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ const router = Router();
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['read:media']),
|
||||
authorize(['products.items']),
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
@@ -57,7 +57,7 @@ router.get(
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['read:media']),
|
||||
authorize(['products.items']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
controller.getById
|
||||
@@ -90,7 +90,7 @@ router.get(
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['write:media']),
|
||||
authorize(['products.items']),
|
||||
createValidation,
|
||||
validate,
|
||||
audit('CREATE_ASSET_FAMILY'),
|
||||
@@ -129,7 +129,7 @@ router.post(
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:media']),
|
||||
authorize(['products.items']),
|
||||
updateValidation,
|
||||
validate,
|
||||
audit('UPDATE_ASSET_FAMILY'),
|
||||
@@ -159,7 +159,7 @@ router.put(
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:media']),
|
||||
authorize(['products.items']),
|
||||
deleteValidation,
|
||||
validate,
|
||||
audit('DELETE_ASSET_FAMILY'),
|
||||
|
||||
@@ -2,6 +2,7 @@ import repository from './assetFamily.repository.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
import { SocketService } from '../../../shared/services/socket.service.js';
|
||||
import { AuditService } from '../../../shared/services/audit.service.js';
|
||||
import { ApiError } from '../../../utils/helpers/ApiError.utils.js';
|
||||
|
||||
export class AssetFamilyService {
|
||||
encodeDescription(text = '', assetTypeIds = []) {
|
||||
@@ -91,7 +92,7 @@ export class AssetFamilyService {
|
||||
async getById(id) {
|
||||
const record = await repository.findById(id);
|
||||
if (!record) {
|
||||
throw new Error('AssetFamily not found');
|
||||
throw new ApiError(404, 'AssetFamily not found');
|
||||
}
|
||||
return await this.enrichFamilyWithTypes(record);
|
||||
}
|
||||
@@ -117,7 +118,7 @@ export class AssetFamilyService {
|
||||
action: 'CREATE',
|
||||
resource: 'AssetFamily',
|
||||
resourceId: record.id,
|
||||
userId: userContext?.id || userContext?.name || 'system',
|
||||
userId: userContext?.user_id || userContext?.name || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
@@ -129,7 +130,7 @@ export class AssetFamilyService {
|
||||
|
||||
const existingRecord = await repository.findById(id);
|
||||
if (!existingRecord) {
|
||||
throw new Error('AssetFamily not found');
|
||||
throw new ApiError(404, 'AssetFamily not found');
|
||||
}
|
||||
|
||||
let currentText = description;
|
||||
@@ -153,7 +154,7 @@ export class AssetFamilyService {
|
||||
action: 'UPDATE',
|
||||
resource: 'AssetFamily',
|
||||
resourceId: id,
|
||||
userId: userContext?.id || userContext?.name || 'system',
|
||||
userId: userContext?.user_id || userContext?.name || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
@@ -163,7 +164,7 @@ export class AssetFamilyService {
|
||||
async delete(id, userContext = {}) {
|
||||
const deleted = await repository.delete(id);
|
||||
if (!deleted) {
|
||||
throw new Error('AssetFamily not found');
|
||||
throw new ApiError(404, 'AssetFamily not found');
|
||||
}
|
||||
|
||||
SocketService.broadcast('assetFamily:deleted', { id });
|
||||
@@ -172,7 +173,7 @@ export class AssetFamilyService {
|
||||
action: 'DELETE',
|
||||
resource: 'AssetFamily',
|
||||
resourceId: id,
|
||||
userId: userContext?.id || userContext?.name || 'system'
|
||||
userId: userContext?.user_id || userContext?.name || 'system'
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
@@ -30,7 +30,7 @@ const router = Router();
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['read:media']),
|
||||
authorize(['products.items']),
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
@@ -57,7 +57,7 @@ router.get(
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['read:media']),
|
||||
authorize(['products.items']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
controller.getById
|
||||
@@ -90,7 +90,7 @@ router.get(
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['write:media']),
|
||||
authorize(['products.items']),
|
||||
createValidation,
|
||||
validate,
|
||||
audit('CREATE_ASSET_TYPE'),
|
||||
@@ -129,7 +129,7 @@ router.post(
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:media']),
|
||||
authorize(['products.items']),
|
||||
updateValidation,
|
||||
validate,
|
||||
audit('UPDATE_ASSET_TYPE'),
|
||||
@@ -159,7 +159,7 @@ router.put(
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:media']),
|
||||
authorize(['products.items']),
|
||||
deleteValidation,
|
||||
validate,
|
||||
audit('DELETE_ASSET_TYPE'),
|
||||
|
||||
@@ -2,6 +2,7 @@ import repository from './assetType.repository.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
import { SocketService } from '../../../shared/services/socket.service.js';
|
||||
import { AuditService } from '../../../shared/services/audit.service.js';
|
||||
import { ApiError } from '../../../utils/helpers/ApiError.utils.js';
|
||||
|
||||
export class AssetTypeService {
|
||||
async getAll(query = {}) {
|
||||
@@ -15,9 +16,7 @@ export class AssetTypeService {
|
||||
async getById(id) {
|
||||
const record = await repository.findById(id);
|
||||
if (!record) {
|
||||
const err = new Error('Asset Type not found');
|
||||
err.statusCode = 404;
|
||||
throw err;
|
||||
throw new ApiError(404, 'AssetType not found');
|
||||
}
|
||||
return record;
|
||||
}
|
||||
@@ -63,7 +62,7 @@ export class AssetTypeService {
|
||||
action: 'CREATE',
|
||||
resource: 'AssetType',
|
||||
resourceId: record.id,
|
||||
userId: userContext.id || 'system',
|
||||
userId: userContext.userId || 'system',
|
||||
details: payload
|
||||
});
|
||||
|
||||
@@ -81,7 +80,7 @@ export class AssetTypeService {
|
||||
if (data.code && data.code !== record.code) {
|
||||
const existing = await models.AssetType.findOne({ where: { code: data.code } });
|
||||
if (existing) {
|
||||
const err = new Error(`Asset Type with code "${data.code}" already exists`);
|
||||
const err = new ApiError(404, `Asset Type with code "${data.code}" already exists`);
|
||||
err.statusCode = 400;
|
||||
throw err;
|
||||
}
|
||||
@@ -100,7 +99,7 @@ export class AssetTypeService {
|
||||
action: 'UPDATE',
|
||||
resource: 'AssetType',
|
||||
resourceId: id,
|
||||
userId: userContext.id || 'system',
|
||||
userId: userContext.userId || 'system',
|
||||
details: payload
|
||||
});
|
||||
|
||||
@@ -125,9 +124,7 @@ export class AssetTypeService {
|
||||
|
||||
const deleted = await repository.delete(id);
|
||||
if (!deleted) {
|
||||
const err = new Error('Failed to delete Asset Type');
|
||||
err.statusCode = 500;
|
||||
throw err;
|
||||
throw new ApiError(404, 'AssetType not found');
|
||||
}
|
||||
|
||||
SocketService.broadcast('assetType:deleted', { id });
|
||||
@@ -136,7 +133,7 @@ export class AssetTypeService {
|
||||
action: 'DELETE',
|
||||
resource: 'AssetType',
|
||||
resourceId: id,
|
||||
userId: userContext.id || 'system'
|
||||
userId: userContext.userId || 'system'
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
@@ -18,7 +18,7 @@ const router = Router();
|
||||
router.get(
|
||||
'/analytics',
|
||||
authenticate,
|
||||
authorize(['read:media']),
|
||||
authorize(['products.items']),
|
||||
controller.getAnalytics
|
||||
);
|
||||
|
||||
@@ -26,7 +26,7 @@ router.get(
|
||||
router.get(
|
||||
'/folders',
|
||||
authenticate,
|
||||
authorize(['read:media']),
|
||||
authorize(['products.items']),
|
||||
controller.getFolders
|
||||
);
|
||||
|
||||
@@ -34,7 +34,7 @@ router.get(
|
||||
router.get(
|
||||
'/tags',
|
||||
authenticate,
|
||||
authorize(['read:media']),
|
||||
authorize(['products.items']),
|
||||
controller.getTags
|
||||
);
|
||||
|
||||
@@ -42,7 +42,7 @@ router.get(
|
||||
router.post(
|
||||
'/upload',
|
||||
authenticate,
|
||||
authorize(['write:media']),
|
||||
authorize(['products.items']),
|
||||
uploadSingle('file'),
|
||||
controller.upload
|
||||
);
|
||||
@@ -51,7 +51,7 @@ router.post(
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['read:media']),
|
||||
authorize(['products.items']),
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
@@ -59,7 +59,7 @@ router.get(
|
||||
router.get(
|
||||
'/:id/relations',
|
||||
authenticate,
|
||||
authorize(['read:media']),
|
||||
authorize(['products.items']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
controller.getRelations
|
||||
@@ -69,7 +69,7 @@ router.get(
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['read:media']),
|
||||
authorize(['products.items']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
controller.getById
|
||||
@@ -79,7 +79,7 @@ router.get(
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['write:media']),
|
||||
authorize(['products.items']),
|
||||
createValidation,
|
||||
validate,
|
||||
audit('CREATE_ASSET'),
|
||||
@@ -90,7 +90,7 @@ router.post(
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:media']),
|
||||
authorize(['products.items']),
|
||||
updateValidation,
|
||||
validate,
|
||||
audit('UPDATE_ASSET'),
|
||||
@@ -101,7 +101,7 @@ router.put(
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:media']),
|
||||
authorize(['products.items']),
|
||||
deleteValidation,
|
||||
validate,
|
||||
audit('DELETE_ASSET'),
|
||||
@@ -112,7 +112,7 @@ router.delete(
|
||||
router.post(
|
||||
'/:id/replace',
|
||||
authenticate,
|
||||
authorize(['write:media']),
|
||||
authorize(['products.items']),
|
||||
uploadSingle('file'),
|
||||
controller.replaceFile
|
||||
);
|
||||
@@ -121,7 +121,7 @@ router.post(
|
||||
router.post(
|
||||
'/:id/archive',
|
||||
authenticate,
|
||||
authorize(['write:media']),
|
||||
authorize(['products.items']),
|
||||
controller.archive
|
||||
);
|
||||
|
||||
@@ -129,7 +129,7 @@ router.post(
|
||||
router.post(
|
||||
'/:id/restore',
|
||||
authenticate,
|
||||
authorize(['write:media']),
|
||||
authorize(['products.items']),
|
||||
controller.restore
|
||||
);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import repository from './asset.repository.js';
|
||||
import { models, sequelize } from '../../../shared/database/models.js';
|
||||
import { SocketService } from '../../../shared/services/socket.service.js';
|
||||
import { AuditService } from '../../../shared/services/audit.service.js';
|
||||
import { ApiError } from '../../../utils/helpers/ApiError.utils.js';
|
||||
import { extractMetadata } from '../../../shared/utils/metadataExtractor.js';
|
||||
import { Op } from 'sequelize';
|
||||
import path from 'path';
|
||||
@@ -55,7 +56,7 @@ export class AssetService {
|
||||
]
|
||||
}, context);
|
||||
if (!record) {
|
||||
throw new Error('Asset not found');
|
||||
throw new ApiError(404, 'Asset not found');
|
||||
}
|
||||
return record;
|
||||
}
|
||||
@@ -148,7 +149,7 @@ export class AssetService {
|
||||
action: 'CREATE',
|
||||
resource: 'Asset',
|
||||
resourceId: record.id,
|
||||
userId: context.userId || 'system',
|
||||
userId: userContext.userId || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
@@ -164,7 +165,7 @@ export class AssetService {
|
||||
try {
|
||||
const record = await models.Asset.findByPk(id, { transaction });
|
||||
if (!record) {
|
||||
throw new Error('Asset not found');
|
||||
throw new ApiError(404, 'Asset not found');
|
||||
}
|
||||
|
||||
if (data.code && data.code !== record.code) {
|
||||
@@ -211,7 +212,7 @@ export class AssetService {
|
||||
action: 'UPDATE',
|
||||
resource: 'Asset',
|
||||
resourceId: id,
|
||||
userId: context.userId || 'system',
|
||||
userId: userContext.userId || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
@@ -326,7 +327,7 @@ export class AssetService {
|
||||
async delete(id, context = {}) {
|
||||
const record = await models.Asset.findByPk(id);
|
||||
if (!record) {
|
||||
throw new Error('Asset not found');
|
||||
throw new ApiError(404, 'Asset not found');
|
||||
}
|
||||
|
||||
// Clean up junction mapping tables so deletion proceeds cleanly
|
||||
@@ -346,7 +347,7 @@ export class AssetService {
|
||||
action: 'DELETE',
|
||||
resource: 'Asset',
|
||||
resourceId: id,
|
||||
userId: context.userId || 'system'
|
||||
userId: userContext.userId || 'system'
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
@@ -26,7 +26,7 @@ const router = Router();
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['read:medias']),
|
||||
authorize(['products.items']),
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@ router.get(
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['read:medias']),
|
||||
authorize(['products.items']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
controller.getById
|
||||
@@ -68,7 +68,7 @@ router.get(
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['write:medias']),
|
||||
authorize(['products.items']),
|
||||
createValidation,
|
||||
validate,
|
||||
audit('CREATE_MEDIA'),
|
||||
@@ -92,7 +92,7 @@ router.post(
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:medias']),
|
||||
authorize(['products.items']),
|
||||
updateValidation,
|
||||
validate,
|
||||
audit('UPDATE_MEDIA'),
|
||||
@@ -116,7 +116,7 @@ router.put(
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:medias']),
|
||||
authorize(['products.items']),
|
||||
deleteValidation,
|
||||
validate,
|
||||
audit('DELETE_MEDIA'),
|
||||
|
||||
@@ -27,7 +27,7 @@ export class MediaService {
|
||||
action: 'CREATE',
|
||||
resource: 'Media',
|
||||
resourceId: record.id,
|
||||
userId: userContext.id || 'system',
|
||||
userId: userContext.userId || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
@@ -46,7 +46,7 @@ export class MediaService {
|
||||
action: 'UPDATE',
|
||||
resource: 'Media',
|
||||
resourceId: id,
|
||||
userId: userContext.id || 'system',
|
||||
userId: userContext.userId || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
@@ -65,7 +65,7 @@ export class MediaService {
|
||||
action: 'DELETE',
|
||||
resource: 'Media',
|
||||
resourceId: id,
|
||||
userId: userContext.id || 'system'
|
||||
userId: userContext.userId || 'system'
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Router } from 'express';
|
||||
import notificationsRouter from './notifications/notification.routes.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use('/notifications', notificationsRouter);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,57 @@
|
||||
import service from './notification.service.js';
|
||||
|
||||
export class NotificationController {
|
||||
async getAll(req, res, next) {
|
||||
try {
|
||||
const result = await service.getAll(req.query, req.user);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Notifications retrieved successfully.',
|
||||
data: result.data,
|
||||
pagination: result.pagination,
|
||||
meta: result.meta
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async getById(req, res, next) {
|
||||
try {
|
||||
const record = await service.getById(req.params.id, req.user);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Notification retrieved successfully.',
|
||||
data: record
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async markRead(req, res, next) {
|
||||
try {
|
||||
await service.markRead(req.params.id, req.user);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Notification marked as read.'
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async markAllRead(req, res, next) {
|
||||
try {
|
||||
await service.markAllRead(req.user);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'All notifications marked as read.'
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new NotificationController();
|
||||
@@ -0,0 +1,5 @@
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
class NotificationEventEmitter extends EventEmitter {}
|
||||
|
||||
export const notificationEvents = new NotificationEventEmitter();
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
|
||||
export class Notification extends Model {
|
||||
static associate(models) {
|
||||
Notification.belongsTo(models.User, {
|
||||
foreignKey: 'recipient_user_id',
|
||||
as: 'recipient',
|
||||
onDelete: 'CASCADE'
|
||||
});
|
||||
Notification.belongsTo(models.User, {
|
||||
foreignKey: 'actor_user_id',
|
||||
as: 'actor',
|
||||
onDelete: 'SET NULL'
|
||||
});
|
||||
Notification.belongsTo(models.Tenant, {
|
||||
foreignKey: 'tenant_id',
|
||||
as: 'tenant',
|
||||
onDelete: 'CASCADE'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default (sequelize) => {
|
||||
Notification.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
tenant_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
},
|
||||
recipient_user_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
actor_user_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
},
|
||||
variant: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false
|
||||
},
|
||||
action: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
entity: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false
|
||||
},
|
||||
entity_id: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false
|
||||
},
|
||||
is_read: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
allowNull: false
|
||||
},
|
||||
metadata: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'Notification',
|
||||
tableName: 'notifications',
|
||||
timestamps: true,
|
||||
underscored: true
|
||||
});
|
||||
|
||||
return Notification;
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class NotificationRepository {
|
||||
async findAndCountAll(options = {}) {
|
||||
return await models.Notification.findAndCountAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await models.Notification.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await models.Notification.create(data, options);
|
||||
}
|
||||
|
||||
async markRead(id, recipientUserId, options = {}) {
|
||||
const record = await models.Notification.findOne({
|
||||
where: { id, recipient_user_id: recipientUserId },
|
||||
...options
|
||||
});
|
||||
if (!record) return null;
|
||||
return await record.update({ is_read: true }, options);
|
||||
}
|
||||
|
||||
async markAllRead(recipientUserId, options = {}) {
|
||||
return await models.Notification.update(
|
||||
{ is_read: true },
|
||||
{
|
||||
where: { recipient_user_id: recipientUserId, is_read: false },
|
||||
...options
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async countUnread(recipientUserId, options = {}) {
|
||||
return await models.Notification.count({
|
||||
where: { recipient_user_id: recipientUserId, is_read: false },
|
||||
...options
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new NotificationRepository();
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Router } from 'express';
|
||||
import controller from './notification.controller.js';
|
||||
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 {
|
||||
getNotificationsValidation,
|
||||
getByIdValidation,
|
||||
markReadValidation
|
||||
} from './notification.validation.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// GET /notifications — list for authenticated user
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['notifications']),
|
||||
getNotificationsValidation,
|
||||
validate,
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
// PATCH /notifications/read-all — static route MUST precede /:id
|
||||
router.patch(
|
||||
'/read-all',
|
||||
authenticate,
|
||||
authorize(['notifications.update']),
|
||||
controller.markAllRead
|
||||
);
|
||||
|
||||
// GET /notifications/:id — single notification
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['notifications']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
controller.getById
|
||||
);
|
||||
|
||||
// PATCH /notifications/:id/read — mark single as read
|
||||
router.patch(
|
||||
'/:id/read',
|
||||
authenticate,
|
||||
authorize(['notifications.update']),
|
||||
markReadValidation,
|
||||
validate,
|
||||
controller.markRead
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,256 @@
|
||||
import { Op } from 'sequelize';
|
||||
import repository from './notification.repository.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
import sequelize from '../../../shared/database/connection.js';
|
||||
import { notificationEvents } from './notification.events.js';
|
||||
|
||||
/**
|
||||
* Maps a Notification Sequelize instance to a DTO.
|
||||
*
|
||||
* Per architecture spec:
|
||||
* - actor / actorAvatar are resolved via the actor association (actor_user_id JOIN)
|
||||
* - link is NOT generated here — the frontend derives navigation URLs from entity + entity_id
|
||||
*/
|
||||
const SORT_COLUMN_MAP = {
|
||||
createdAt: 'created_at',
|
||||
isRead: 'is_read',
|
||||
variant: 'variant',
|
||||
action: 'action'
|
||||
};
|
||||
|
||||
const mapToDTO = (notification) => ({
|
||||
id: notification.id,
|
||||
variant: notification.variant,
|
||||
action: notification.action,
|
||||
title: notification.title,
|
||||
description: notification.description,
|
||||
entity: notification.entity,
|
||||
entityId: notification.entity_id,
|
||||
actor: notification.actor ? notification.actor.user_name : 'System',
|
||||
actorAvatar: notification.actor ? notification.actor.profile_picture : null,
|
||||
isRead: notification.is_read,
|
||||
createdAt: notification.created_at
|
||||
});
|
||||
|
||||
const actorInclude = () => ({
|
||||
model: models.User,
|
||||
as: 'actor',
|
||||
attributes: ['id', 'user_name', 'profile_picture']
|
||||
});
|
||||
|
||||
export class NotificationService {
|
||||
/**
|
||||
* List notifications for the authenticated user with filtering, pagination, and sorting.
|
||||
*/
|
||||
async getAll(query = {}, userContext = {}) {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 20,
|
||||
search,
|
||||
variant,
|
||||
action,
|
||||
isRead,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
sortBy = 'createdAt',
|
||||
sortOrder = 'desc'
|
||||
} = query;
|
||||
|
||||
const offset = (parseInt(page, 10) - 1) * parseInt(limit, 10);
|
||||
|
||||
const where = {
|
||||
recipient_user_id: userContext.user_id
|
||||
};
|
||||
|
||||
if (userContext.user_type === 'tenant') {
|
||||
where.tenant_id = userContext.tenant_id;
|
||||
}
|
||||
|
||||
if (search && search.trim()) {
|
||||
const searchPattern = `%${search.trim()}%`;
|
||||
where[Op.or] = [
|
||||
{ title: { [Op.iLike]: searchPattern } },
|
||||
{ description: { [Op.iLike]: searchPattern } }
|
||||
];
|
||||
}
|
||||
|
||||
if (variant && variant !== 'all') {
|
||||
where.variant = variant;
|
||||
}
|
||||
|
||||
if (action) {
|
||||
where.action = action;
|
||||
}
|
||||
|
||||
if (isRead === 'true' || isRead === true) {
|
||||
where.is_read = true;
|
||||
} else if (isRead === 'false' || isRead === false) {
|
||||
where.is_read = false;
|
||||
}
|
||||
|
||||
if (dateFrom || dateTo) {
|
||||
where.created_at = {};
|
||||
if (dateFrom) where.created_at[Op.gte] = new Date(dateFrom);
|
||||
if (dateTo) where.created_at[Op.lte] = new Date(dateTo);
|
||||
}
|
||||
|
||||
const dbSortField = SORT_COLUMN_MAP[sortBy] ?? 'created_at';
|
||||
|
||||
const { rows, count } = await repository.findAndCountAll({
|
||||
where,
|
||||
limit: parseInt(limit, 10),
|
||||
offset,
|
||||
order: [[dbSortField, sortOrder.toUpperCase()]],
|
||||
include: [actorInclude()]
|
||||
});
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(count / parseInt(limit, 10)));
|
||||
const unreadCount = await repository.countUnread(userContext.user_id);
|
||||
|
||||
return {
|
||||
data: rows.map(mapToDTO),
|
||||
pagination: {
|
||||
page: parseInt(page, 10),
|
||||
limit: parseInt(limit, 10),
|
||||
total: count,
|
||||
totalPages,
|
||||
hasNextPage: parseInt(page, 10) < totalPages,
|
||||
hasPreviousPage: parseInt(page, 10) > 1
|
||||
},
|
||||
meta: { unreadCount }
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a single notification by id, scoped to the authenticated user.
|
||||
*/
|
||||
async getById(id, userContext = {}) {
|
||||
const where = { id, recipient_user_id: userContext.user_id };
|
||||
|
||||
if (userContext.user_type === 'tenant') {
|
||||
where.tenant_id = userContext.tenant_id;
|
||||
}
|
||||
|
||||
const notification = await models.Notification.findOne({
|
||||
where,
|
||||
include: [actorInclude()]
|
||||
});
|
||||
|
||||
if (!notification) {
|
||||
const err = new Error('Notification not found');
|
||||
err.statusCode = 404;
|
||||
throw err;
|
||||
}
|
||||
|
||||
return mapToDTO(notification);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a single notification as read, then broadcast the updated unread count.
|
||||
*/
|
||||
async markRead(id, userContext = {}) {
|
||||
const updated = await repository.markRead(id, userContext.user_id);
|
||||
if (!updated) {
|
||||
const err = new Error('Notification not found');
|
||||
err.statusCode = 404;
|
||||
throw err;
|
||||
}
|
||||
|
||||
const count = await repository.countUnread(userContext.user_id);
|
||||
notificationEvents.emit('notification:unread-count', {
|
||||
recipientUserId: userContext.user_id,
|
||||
count
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all unread notifications as read for the authenticated user.
|
||||
* Uses a transaction; the socket broadcast fires only after commit.
|
||||
*/
|
||||
async markAllRead(userContext = {}) {
|
||||
const recipientUserId = userContext.user_id;
|
||||
|
||||
await sequelize.transaction(async (t) => {
|
||||
await repository.markAllRead(recipientUserId, { transaction: t });
|
||||
const count = await repository.countUnread(recipientUserId, { transaction: t });
|
||||
|
||||
t.afterCommit(() => {
|
||||
notificationEvents.emit('notification:unread-count', {
|
||||
recipientUserId,
|
||||
count
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Core notification creation method.
|
||||
*
|
||||
* Called ONLY by NotificationService itself (via notifyTenant or directly).
|
||||
* Business services must never call repository.create() directly.
|
||||
*
|
||||
* Flow:
|
||||
* repository.create() → DB persist → fetch with actor join → emit socket events
|
||||
*/
|
||||
async createNotification(data) {
|
||||
const record = await repository.create(data);
|
||||
|
||||
const notification = await models.Notification.findOne({
|
||||
where: { id: record.id },
|
||||
include: [actorInclude()]
|
||||
});
|
||||
|
||||
const mapped = mapToDTO(notification);
|
||||
|
||||
// recipient_user_id comes from the raw data, not the DTO (DTO omits it by design)
|
||||
const recipientUserId = data.recipient_user_id;
|
||||
const count = await repository.countUnread(recipientUserId);
|
||||
|
||||
notificationEvents.emit('notification:created', {
|
||||
recipientUserId,
|
||||
notification: mapped
|
||||
});
|
||||
|
||||
notificationEvents.emit('notification:unread-count', {
|
||||
recipientUserId,
|
||||
count
|
||||
});
|
||||
|
||||
return mapped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast a notification to all users in a tenant, excluding the actor.
|
||||
*
|
||||
* Called by business services after a successful committed transaction.
|
||||
* This method is the ONLY authorised entry point for business-module notification generation.
|
||||
*
|
||||
* Recipient resolution:
|
||||
* - All active users belonging to the tenant
|
||||
* - Actor (actorUserId) is excluded — they performed the action
|
||||
*/
|
||||
async notifyTenant(tenantId, actorUserId, notificationData) {
|
||||
const whereClause = { tenant_id: tenantId };
|
||||
|
||||
const users = await models.User.findAll({ where: whereClause });
|
||||
|
||||
const creations = users.map((user) =>
|
||||
this.createNotification({
|
||||
tenant_id: tenantId,
|
||||
recipient_user_id: user.id,
|
||||
actor_user_id: actorUserId || null,
|
||||
...notificationData
|
||||
}).catch((err) => {
|
||||
console.error(`[NotificationService] Failed to notify user ${user.id}:`, err.message);
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(creations);
|
||||
}
|
||||
}
|
||||
|
||||
export default new NotificationService();
|
||||
@@ -0,0 +1,67 @@
|
||||
import { query, param } from 'express-validator';
|
||||
|
||||
export const getNotificationsValidation = [
|
||||
query('page')
|
||||
.optional()
|
||||
.isInt({ min: 1 })
|
||||
.withMessage('Page must be a positive integer')
|
||||
.toInt(),
|
||||
query('limit')
|
||||
.optional()
|
||||
.isInt({ min: 1, max: 100 })
|
||||
.withMessage('Limit must be an integer between 1 and 100')
|
||||
.toInt(),
|
||||
query('search')
|
||||
.optional()
|
||||
.isString()
|
||||
.trim()
|
||||
.isLength({ max: 200 })
|
||||
.withMessage('Search query too long'),
|
||||
query('variant')
|
||||
.optional()
|
||||
.isIn(['all', 'product', 'category', 'brand', 'family', 'attribute', 'store', 'role', 'workflow', 'system'])
|
||||
.withMessage('Invalid variant type'),
|
||||
query('action')
|
||||
.optional()
|
||||
.isIn(['created', 'updated', 'deleted', 'published', 'approved', 'rejected'])
|
||||
.withMessage('Invalid action type'),
|
||||
query('isRead')
|
||||
.optional()
|
||||
.isIn(['all', 'true', 'false'])
|
||||
.withMessage('isRead must be one of: all, true, false'),
|
||||
query('dateFrom')
|
||||
.optional()
|
||||
.isISO8601()
|
||||
.withMessage('dateFrom must be a valid ISO8601 date'),
|
||||
query('dateTo')
|
||||
.optional()
|
||||
.isISO8601()
|
||||
.withMessage('dateTo must be a valid ISO8601 date')
|
||||
.custom((value, { req }) => {
|
||||
if (req.query.dateFrom && new Date(value) < new Date(req.query.dateFrom)) {
|
||||
throw new Error('dateTo must not be before dateFrom');
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
query('sortBy')
|
||||
.optional()
|
||||
.isIn(['createdAt', 'variant', 'action', 'isRead'])
|
||||
.withMessage('Invalid sort field'),
|
||||
query('sortOrder')
|
||||
.optional()
|
||||
.toLowerCase()
|
||||
.isIn(['asc', 'desc'])
|
||||
.withMessage('sortOrder must be asc or desc')
|
||||
];
|
||||
|
||||
export const getByIdValidation = [
|
||||
param('id')
|
||||
.isUUID(4)
|
||||
.withMessage('Valid UUID v4 is required')
|
||||
];
|
||||
|
||||
export const markReadValidation = [
|
||||
param('id')
|
||||
.isUUID(4)
|
||||
.withMessage('Valid UUID v4 is required')
|
||||
];
|
||||
@@ -26,7 +26,7 @@ const router = Router();
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['read:orgs']),
|
||||
authorize(['TENANTS_MANAGEMENT']),
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@ router.get(
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['read:orgs']),
|
||||
authorize(['TENANTS_MANAGEMENT']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
controller.getById
|
||||
@@ -68,7 +68,7 @@ router.get(
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['write:orgs']),
|
||||
authorize(['TENANTS_MANAGEMENT']),
|
||||
createValidation,
|
||||
validate,
|
||||
audit('CREATE_ORG'),
|
||||
@@ -92,7 +92,7 @@ router.post(
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:orgs']),
|
||||
authorize(['TENANTS_MANAGEMENT']),
|
||||
updateValidation,
|
||||
validate,
|
||||
audit('UPDATE_ORG'),
|
||||
@@ -116,7 +116,7 @@ router.put(
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:orgs']),
|
||||
authorize(['TENANTS_MANAGEMENT']),
|
||||
deleteValidation,
|
||||
validate,
|
||||
audit('DELETE_ORG'),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import repository from './org.repository.js';
|
||||
import { SocketService } from '../../../shared/services/socket.service.js';
|
||||
import { AuditService } from '../../../shared/services/audit.service.js';
|
||||
import { ApiError } from '../../../utils/helpers/ApiError.utils.js';
|
||||
|
||||
export class OrgService {
|
||||
async getAll(query = {}) {
|
||||
@@ -11,7 +12,7 @@ export class OrgService {
|
||||
async getById(id) {
|
||||
const record = await repository.findById(id);
|
||||
if (!record) {
|
||||
throw new Error('Org not found');
|
||||
throw new ApiError(404, 'Org not found');
|
||||
}
|
||||
return record;
|
||||
}
|
||||
@@ -30,7 +31,7 @@ export class OrgService {
|
||||
action: 'CREATE',
|
||||
resource: 'Org',
|
||||
resourceId: record.id,
|
||||
userId: userContext.id || 'system',
|
||||
userId: userContext.userId || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
@@ -40,7 +41,7 @@ export class OrgService {
|
||||
async update(id, data, userContext = {}) {
|
||||
const record = await repository.update(id, data);
|
||||
if (!record) {
|
||||
throw new Error('Org not found');
|
||||
throw new ApiError(404, 'Org not found');
|
||||
}
|
||||
|
||||
SocketService.broadcast('org:updated', record);
|
||||
@@ -49,7 +50,7 @@ export class OrgService {
|
||||
action: 'UPDATE',
|
||||
resource: 'Org',
|
||||
resourceId: id,
|
||||
userId: userContext.id || 'system',
|
||||
userId: userContext.userId || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
@@ -59,7 +60,7 @@ export class OrgService {
|
||||
async delete(id, userContext = {}) {
|
||||
const deleted = await repository.delete(id);
|
||||
if (!deleted) {
|
||||
throw new Error('Org not found');
|
||||
throw new ApiError(404, 'Org not found');
|
||||
}
|
||||
|
||||
SocketService.broadcast('org:deleted', { id });
|
||||
@@ -68,7 +69,7 @@ export class OrgService {
|
||||
action: 'DELETE',
|
||||
resource: 'Org',
|
||||
resourceId: id,
|
||||
userId: userContext.id || 'system'
|
||||
userId: userContext.userId || 'system'
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
@@ -22,7 +22,7 @@ const router = Router();
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['settings.tenants.view']),
|
||||
authorize(['TENANTS_MANAGEMENT']),
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@ router.get(
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['settings.tenants.view']),
|
||||
authorize(['TENANTS_MANAGEMENT']),
|
||||
controller.getById
|
||||
);
|
||||
|
||||
@@ -82,7 +82,7 @@ router.get(
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['settings.tenants.create']),
|
||||
authorize(['TENANTS_MANAGEMENT']),
|
||||
controller.create
|
||||
);
|
||||
|
||||
@@ -107,7 +107,7 @@ router.post(
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['settings.tenants.edit']),
|
||||
authorize(['TENANTS_MANAGEMENT']),
|
||||
controller.update
|
||||
);
|
||||
|
||||
@@ -132,7 +132,7 @@ router.put(
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['settings.tenants.delete']),
|
||||
authorize(['TENANTS_MANAGEMENT']),
|
||||
controller.delete
|
||||
);
|
||||
|
||||
|
||||
@@ -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 './product.validation.js';
|
||||
|
||||
const router = Router();
|
||||
@@ -26,7 +26,7 @@ const router = Router();
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['read:products']),
|
||||
authorize(['products.items']),
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@ router.get(
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['read:products']),
|
||||
authorize(['products.items']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
controller.getById
|
||||
@@ -68,7 +68,7 @@ router.get(
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['write:products']),
|
||||
authorize(['products.items']),
|
||||
createValidation,
|
||||
validate,
|
||||
audit('CREATE_PRODUCT'),
|
||||
@@ -92,7 +92,7 @@ router.post(
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:products']),
|
||||
authorize(['products.items']),
|
||||
updateValidation,
|
||||
validate,
|
||||
audit('UPDATE_PRODUCT'),
|
||||
@@ -116,7 +116,7 @@ router.put(
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:products']),
|
||||
authorize(['products.items']),
|
||||
deleteValidation,
|
||||
validate,
|
||||
audit('DELETE_PRODUCT'),
|
||||
@@ -126,35 +126,35 @@ router.delete(
|
||||
router.get(
|
||||
'/:id/assets',
|
||||
authenticate,
|
||||
authorize(['read:products']),
|
||||
authorize(['products.items']),
|
||||
controller.getAssets
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/:id/assets',
|
||||
authenticate,
|
||||
authorize(['write:products']),
|
||||
authorize(['products.items']),
|
||||
controller.assignAsset
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/:id/assets/:assetId',
|
||||
authenticate,
|
||||
authorize(['write:products']),
|
||||
authorize(['products.items']),
|
||||
controller.updateAssetMapping
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:id/assets/:assetId',
|
||||
authenticate,
|
||||
authorize(['write:products']),
|
||||
authorize(['products.items']),
|
||||
controller.unassignAsset
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/:id/archive',
|
||||
authenticate,
|
||||
authorize(['write:products']),
|
||||
authorize(['products.items']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
audit('ARCHIVE_PRODUCT'),
|
||||
@@ -164,7 +164,7 @@ router.post(
|
||||
router.post(
|
||||
'/:id/restore',
|
||||
authenticate,
|
||||
authorize(['write:products']),
|
||||
authorize(['products.items']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
audit('RESTORE_PRODUCT'),
|
||||
|
||||
@@ -3,6 +3,8 @@ import repository from './product.repository.js';
|
||||
import { models, sequelize } from '../../../shared/database/models.js';
|
||||
import { SocketService } from '../../../shared/services/socket.service.js';
|
||||
import { AuditService } from '../../../shared/services/audit.service.js';
|
||||
import NotificationService from '../../notifications/notifications/notification.service.js';
|
||||
import { ApiError } from '../../../utils/helpers/ApiError.utils.js';
|
||||
import CompletenessService from './completeness.service.js';
|
||||
|
||||
async function validateAttributeValue(attr, val, productId = null, transaction = null) {
|
||||
@@ -229,7 +231,7 @@ export class ProductService {
|
||||
async getById(id, context = {}) {
|
||||
const record = await repository.findById(id, {}, context);
|
||||
if (!record) {
|
||||
throw new Error('Product not found');
|
||||
throw new ApiError(404, 'Product not found');
|
||||
}
|
||||
|
||||
const json = record.toJSON();
|
||||
@@ -404,10 +406,22 @@ export class ProductService {
|
||||
action: 'CREATE',
|
||||
resource: 'Product',
|
||||
resourceId: product.id,
|
||||
userId: context.userId || 'system',
|
||||
userId: userContext.userId || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
// Notify tenant users
|
||||
if (userContext.tenantId) {
|
||||
NotificationService.notifyTenant(userContext.tenantId, userContext.userId, {
|
||||
variant: 'product',
|
||||
action: 'created',
|
||||
title: 'New product created',
|
||||
description: `"${product.name || 'A product'}" was added to the catalog.`,
|
||||
entity: product.name || 'Product',
|
||||
entity_id: product.id.toString()
|
||||
}).catch(err => console.error('Notification failed:', err));
|
||||
}
|
||||
|
||||
return fullRecord;
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
@@ -420,7 +434,7 @@ export class ProductService {
|
||||
try {
|
||||
const record = await repository.findById(id, { transaction }, context);
|
||||
if (!record) {
|
||||
throw new Error('Product not found');
|
||||
throw new ApiError(404, 'Product not found');
|
||||
}
|
||||
|
||||
// Validate Family
|
||||
@@ -554,10 +568,22 @@ export class ProductService {
|
||||
action: 'UPDATE',
|
||||
resource: 'Product',
|
||||
resourceId: id,
|
||||
userId: context.userId || 'system',
|
||||
userId: userContext.userId || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
// Notify tenant users
|
||||
if (userContext.tenantId) {
|
||||
NotificationService.notifyTenant(userContext.tenantId, userContext.userId, {
|
||||
variant: 'product',
|
||||
action: 'updated',
|
||||
title: 'Product updated',
|
||||
description: `"${record.name || 'A product'}" was updated.`,
|
||||
entity: record.name || 'Product',
|
||||
entity_id: id.toString()
|
||||
}).catch(err => console.error('Notification failed:', err));
|
||||
}
|
||||
|
||||
return fullRecord;
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
@@ -568,7 +594,7 @@ export class ProductService {
|
||||
async delete(id, context = {}) {
|
||||
const record = await repository.findById(id, {}, context);
|
||||
if (!record) {
|
||||
throw new Error('Product not found');
|
||||
throw new ApiError(404, 'Product not found');
|
||||
}
|
||||
|
||||
await repository.delete(id, {}, context);
|
||||
@@ -579,9 +605,21 @@ export class ProductService {
|
||||
action: 'DELETE',
|
||||
resource: 'Product',
|
||||
resourceId: id,
|
||||
userId: context.userId || 'system'
|
||||
userId: userContext.userId || userContext.id || 'system'
|
||||
});
|
||||
|
||||
// Notify tenant users
|
||||
if (userContext.tenantId) {
|
||||
NotificationService.notifyTenant(userContext.tenantId, userContext.userId, {
|
||||
variant: 'product',
|
||||
action: 'deleted',
|
||||
title: 'Product deleted',
|
||||
description: 'A product was permanently removed from the catalog.',
|
||||
entity: 'Product',
|
||||
entity_id: id.toString()
|
||||
}).catch(err => console.error('Notification failed:', err));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -599,9 +637,21 @@ export class ProductService {
|
||||
action: 'ARCHIVE',
|
||||
resource: 'Product',
|
||||
resourceId: id,
|
||||
userId: context.userId || 'system'
|
||||
userId: userContext.userId || userContext.id || 'system'
|
||||
});
|
||||
|
||||
// Notify tenant users
|
||||
if (userContext.tenantId) {
|
||||
NotificationService.notifyTenant(userContext.tenantId, userContext.userId, {
|
||||
variant: 'product',
|
||||
action: 'deleted',
|
||||
title: 'Product deleted',
|
||||
description: 'A product was permanently removed from the catalog.',
|
||||
entity: 'Product',
|
||||
entity_id: id.toString()
|
||||
}).catch(err => console.error('Notification failed:', err));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 './setting.validation.js';
|
||||
|
||||
const router = Router();
|
||||
@@ -26,7 +26,7 @@ const router = Router();
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['read:settings']),
|
||||
authorize(['settings.users']),
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@ router.get(
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['read:settings']),
|
||||
authorize(['settings.users']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
controller.getById
|
||||
@@ -68,7 +68,7 @@ router.get(
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['write:settings']),
|
||||
authorize(['settings.users']),
|
||||
createValidation,
|
||||
validate,
|
||||
audit('CREATE_SETTING'),
|
||||
@@ -92,7 +92,7 @@ router.post(
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:settings']),
|
||||
authorize(['settings.users']),
|
||||
updateValidation,
|
||||
validate,
|
||||
audit('UPDATE_SETTING'),
|
||||
@@ -116,7 +116,7 @@ router.put(
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:settings']),
|
||||
authorize(['settings.users']),
|
||||
deleteValidation,
|
||||
validate,
|
||||
audit('DELETE_SETTING'),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import repository from './setting.repository.js';
|
||||
import { SocketService } from '../../../shared/services/socket.service.js';
|
||||
import { AuditService } from '../../../shared/services/audit.service.js';
|
||||
import { ApiError } from '../../../utils/helpers/ApiError.utils.js';
|
||||
|
||||
export class SettingService {
|
||||
async getAll(query = {}) {
|
||||
@@ -11,7 +12,7 @@ export class SettingService {
|
||||
async getById(id) {
|
||||
const record = await repository.findById(id);
|
||||
if (!record) {
|
||||
throw new Error('Setting not found');
|
||||
throw new ApiError(404, 'Setting not found');
|
||||
}
|
||||
return record;
|
||||
}
|
||||
@@ -27,7 +28,7 @@ export class SettingService {
|
||||
action: 'CREATE',
|
||||
resource: 'Setting',
|
||||
resourceId: record.id,
|
||||
userId: userContext.id || 'system',
|
||||
userId: userContext.userId || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
@@ -37,7 +38,7 @@ export class SettingService {
|
||||
async update(id, data, userContext = {}) {
|
||||
const record = await repository.update(id, data);
|
||||
if (!record) {
|
||||
throw new Error('Setting not found');
|
||||
throw new ApiError(404, 'Setting not found');
|
||||
}
|
||||
|
||||
SocketService.broadcast('setting:updated', record);
|
||||
@@ -46,7 +47,7 @@ export class SettingService {
|
||||
action: 'UPDATE',
|
||||
resource: 'Setting',
|
||||
resourceId: id,
|
||||
userId: userContext.id || 'system',
|
||||
userId: userContext.userId || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
@@ -56,7 +57,7 @@ export class SettingService {
|
||||
async delete(id, userContext = {}) {
|
||||
const deleted = await repository.delete(id);
|
||||
if (!deleted) {
|
||||
throw new Error('Setting not found');
|
||||
throw new ApiError(404, 'Setting not found');
|
||||
}
|
||||
|
||||
SocketService.broadcast('setting:deleted', { id });
|
||||
@@ -65,7 +66,7 @@ export class SettingService {
|
||||
action: 'DELETE',
|
||||
resource: 'Setting',
|
||||
resourceId: id,
|
||||
userId: userContext.id || 'system'
|
||||
userId: userContext.userId || 'system'
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
@@ -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 './variant.validation.js';
|
||||
|
||||
const router = Router();
|
||||
@@ -26,7 +26,7 @@ const router = Router();
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['read:products']),
|
||||
authorize(['products.variants']),
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
@@ -65,7 +65,7 @@ router.post(
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['read:products']),
|
||||
authorize(['products.variants']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
controller.getById
|
||||
@@ -84,7 +84,7 @@ router.get(
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['write:products']),
|
||||
authorize(['products.variants']),
|
||||
createValidation,
|
||||
validate,
|
||||
audit('CREATE_VARIANT'),
|
||||
@@ -108,7 +108,7 @@ router.post(
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:products']),
|
||||
authorize(['products.variants']),
|
||||
updateValidation,
|
||||
validate,
|
||||
audit('UPDATE_VARIANT'),
|
||||
@@ -132,7 +132,7 @@ router.put(
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:products']),
|
||||
authorize(['products.variants']),
|
||||
deleteValidation,
|
||||
validate,
|
||||
audit('DELETE_VARIANT'),
|
||||
@@ -156,7 +156,7 @@ router.delete(
|
||||
router.post(
|
||||
'/:id/archive',
|
||||
authenticate,
|
||||
authorize(['write:products']),
|
||||
authorize(['products.variants']),
|
||||
deleteValidation,
|
||||
validate,
|
||||
audit('ARCHIVE_VARIANT'),
|
||||
@@ -180,7 +180,7 @@ router.post(
|
||||
router.post(
|
||||
'/:id/restore',
|
||||
authenticate,
|
||||
authorize(['write:products']),
|
||||
authorize(['products.variants']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
audit('RESTORE_VARIANT'),
|
||||
@@ -190,28 +190,28 @@ router.post(
|
||||
router.get(
|
||||
'/:id/assets',
|
||||
authenticate,
|
||||
authorize(['read:products']),
|
||||
authorize(['products.variants']),
|
||||
controller.getAssets
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/:id/assets',
|
||||
authenticate,
|
||||
authorize(['write:products']),
|
||||
authorize(['products.variants']),
|
||||
controller.assignAsset
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/:id/assets/:assetId',
|
||||
authenticate,
|
||||
authorize(['write:products']),
|
||||
authorize(['products.variants']),
|
||||
controller.updateAssetMapping
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:id/assets/:assetId',
|
||||
authenticate,
|
||||
authorize(['write:products']),
|
||||
authorize(['products.variants']),
|
||||
controller.unassignAsset
|
||||
);
|
||||
|
||||
|
||||
@@ -399,7 +399,11 @@ export class VariantService {
|
||||
throw new Error('Variant not found');
|
||||
}
|
||||
|
||||
await record.destroy({ transaction });
|
||||
// Hard delete variant values mapping first
|
||||
await models.VariantValue.destroy({ where: { variant_id: id }, transaction });
|
||||
|
||||
// Hard delete variant
|
||||
await record.destroy({ force: true, transaction });
|
||||
await transaction.commit();
|
||||
|
||||
// Emit socket event
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.createTable('notifications', {
|
||||
id: {
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
type: Sequelize.UUID,
|
||||
defaultValue: Sequelize.UUIDV4
|
||||
},
|
||||
tenant_id: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'tenants',
|
||||
key: 'id'
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE'
|
||||
},
|
||||
recipient_user_id: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE'
|
||||
},
|
||||
actor_user_id: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'SET NULL'
|
||||
},
|
||||
variant: {
|
||||
type: Sequelize.STRING(50),
|
||||
allowNull: false
|
||||
},
|
||||
action: {
|
||||
type: Sequelize.STRING(50),
|
||||
allowNull: false
|
||||
},
|
||||
title: {
|
||||
type: Sequelize.STRING(255),
|
||||
allowNull: false
|
||||
},
|
||||
description: {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
entity: {
|
||||
type: Sequelize.STRING(255),
|
||||
allowNull: false
|
||||
},
|
||||
entity_id: {
|
||||
type: Sequelize.STRING(100),
|
||||
allowNull: false
|
||||
},
|
||||
is_read: {
|
||||
type: Sequelize.BOOLEAN,
|
||||
defaultValue: false,
|
||||
allowNull: false
|
||||
},
|
||||
metadata: {
|
||||
type: Sequelize.JSONB,
|
||||
allowNull: true
|
||||
},
|
||||
created_at: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
||||
},
|
||||
updated_at: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
||||
}
|
||||
});
|
||||
|
||||
// Indexes
|
||||
await queryInterface.addIndex('notifications', ['recipient_user_id', 'is_read'], {
|
||||
name: 'idx_notifications_recipient_is_read'
|
||||
});
|
||||
await queryInterface.addIndex('notifications', ['recipient_user_id', 'created_at'], {
|
||||
name: 'idx_notifications_recipient_created_at'
|
||||
});
|
||||
await queryInterface.addIndex('notifications', ['tenant_id', 'recipient_user_id'], {
|
||||
name: 'idx_notifications_tenant_recipient'
|
||||
});
|
||||
await queryInterface.addIndex('notifications', ['tenant_id', 'recipient_user_id', 'created_at'], {
|
||||
name: 'idx_notifications_tenant_recipient_created_at'
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
await queryInterface.dropTable('notifications');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('auditlogs', {
|
||||
id: {
|
||||
type: Sequelize.UUID,
|
||||
defaultValue: Sequelize.UUIDV4,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
action: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
resource: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
resource_id: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
user_id: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true
|
||||
},
|
||||
old_value: {
|
||||
type: Sequelize.JSONB,
|
||||
allowNull: true
|
||||
},
|
||||
new_value: {
|
||||
type: Sequelize.JSONB,
|
||||
allowNull: true
|
||||
},
|
||||
details: {
|
||||
type: Sequelize.JSONB,
|
||||
allowNull: true
|
||||
},
|
||||
status: {
|
||||
type: Sequelize.STRING,
|
||||
defaultValue: 'success'
|
||||
},
|
||||
created_at: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
||||
},
|
||||
updated_at: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
||||
}
|
||||
});
|
||||
|
||||
await queryInterface.addIndex('auditlogs', ['user_id']);
|
||||
await queryInterface.addIndex('auditlogs', ['resource', 'resource_id']);
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('auditlogs');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Migration: seed-application-permission-nodes
|
||||
*
|
||||
* Idempotent: uses ON CONFLICT (node_code) DO NOTHING so it is safe to run
|
||||
* even when the data has already been inserted.
|
||||
*/
|
||||
module.exports = {
|
||||
async up(queryInterface) {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
// ── Parent groups (node_level: 1) ─────────────────────────────────────────
|
||||
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, can_alter, can_export, can_import, created_at, updated_at)
|
||||
VALUES
|
||||
('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}')
|
||||
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')`,
|
||||
{ type: queryInterface.sequelize.QueryTypes.SELECT }
|
||||
);
|
||||
const pid = {};
|
||||
parents.forEach(r => { pid[r.node_code] = r.id; });
|
||||
|
||||
// ── Child nodes (node_level: 2) ───────────────────────────────────────────
|
||||
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, can_alter, can_export, can_import, created_at, updated_at)
|
||||
VALUES
|
||||
('products.items', 'Products', 'feature', '${pid['products']}', 2, 11, true, true, true, true, true, true, true, '${now}', '${now}'),
|
||||
('products.variants', 'Variant Management', 'feature', '${pid['products']}', 2, 12, true, true, true, true, true, true, true, '${now}', '${now}'),
|
||||
('products.families', 'Families', 'feature', '${pid['products']}', 2, 13, true, true, true, true, false, false, false, '${now}', '${now}'),
|
||||
('products.categories', 'Categories', 'feature', '${pid['products']}', 2, 14, true, true, true, true, false, false, false, '${now}', '${now}'),
|
||||
('products.attributes', 'Attributes', 'feature', '${pid['products']}', 2, 15, true, true, true, true, false, false, false, '${now}', '${now}'),
|
||||
('masters.brands', 'Brands', 'feature', '${pid['masters']}', 2, 21, true, true, true, true, false, false, false, '${now}', '${now}'),
|
||||
('masters.units', 'Units', 'feature', '${pid['masters']}', 2, 22, true, true, true, true, false, false, false, '${now}', '${now}'),
|
||||
('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;
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.bulkDelete('permission_nodes', {
|
||||
node_code: [
|
||||
'products', 'masters', 'settings', 'reports',
|
||||
'products.items', 'products.variants', 'products.families',
|
||||
'products.categories', 'products.attributes',
|
||||
'masters.brands', 'masters.units',
|
||||
'settings.users', 'settings.roles', 'settings.integrations',
|
||||
'reports.sales', 'reports.inventory',
|
||||
],
|
||||
}, {});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface) {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
// Find the settings parent group id
|
||||
const parents = await queryInterface.sequelize.query(
|
||||
`SELECT id FROM permission_nodes WHERE node_code = 'settings'`,
|
||||
{ type: queryInterface.sequelize.QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
const parentId = parents.length > 0 ? parents[0].id : null;
|
||||
|
||||
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, can_alter, can_export, can_import, created_at, updated_at)
|
||||
VALUES (
|
||||
'notifications', 'Notifications', 'feature', ${parentId ? `'${parentId}'` : 'NULL'}, 2, 34,
|
||||
true, false, true, false, false, false, false, '${now}', '${now}'
|
||||
)
|
||||
ON CONFLICT (node_code) DO NOTHING;
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.bulkDelete('permission_nodes', {
|
||||
node_code: 'notifications'
|
||||
}, {});
|
||||
}
|
||||
};
|
||||
@@ -155,6 +155,7 @@ import variantValueModelInit from '../../features/variants/variants/variantValue
|
||||
import assetTypeModelInit from '../../features/media/assetTypes/assetType.model.js';
|
||||
import assetFamilyModelInit from '../../features/media/assetFamilies/assetFamily.model.js';
|
||||
import assetModelInit from '../../features/media/assets/asset.model.js';
|
||||
import notificationModelInit from '../../features/notifications/notifications/notification.model.js';
|
||||
import assetFolderModelInit from '../../features/media/assets/assetFolder.model.js';
|
||||
import tagModelInit from '../../features/media/assets/tag.model.js';
|
||||
import assetTagModelInit from '../../features/media/assets/assetTag.model.js';
|
||||
@@ -223,6 +224,7 @@ export const initializeDatabaseModels = () => {
|
||||
registerModel('AssetType', assetTypeModelInit);
|
||||
registerModel('AssetFamily', assetFamilyModelInit);
|
||||
registerModel('Asset', assetModelInit);
|
||||
registerModel('Notification', notificationModelInit);
|
||||
registerModel('AssetFolder', assetFolderModelInit);
|
||||
registerModel('Tag', tagModelInit);
|
||||
registerModel('AssetTag', assetTagModelInit);
|
||||
|
||||
@@ -17,7 +17,8 @@ export const audit = (action) => {
|
||||
action,
|
||||
resource: req.baseUrl.split('/').pop(),
|
||||
resourceId: req.params.id || (parsedBody.data ? parsedBody.data.id : null),
|
||||
userId: req.user ? req.user.id : 'anonymous',
|
||||
userId: req.user ? req.user.user_id : 'anonymous',
|
||||
new_value: req.body && Object.keys(req.body).length ? req.body : null,
|
||||
details: {
|
||||
method: req.method,
|
||||
query: req.query,
|
||||
|
||||
@@ -5,6 +5,7 @@ export const validate = (req, res, next) => {
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(422).json({
|
||||
success: false,
|
||||
message: `Validation Error: ${errors.array()[0].msg}`,
|
||||
errors: errors.array().map(err => ({ field: err.path, message: err.msg }))
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,31 @@
|
||||
import AuditLogRepository from '../../features/auditLogs/auditLogs/auditLog.repository.js';
|
||||
|
||||
export class AuditService {
|
||||
static async log(auditData) {
|
||||
// In absolute production code, this inserts a record into AuditLog table
|
||||
console.log('[AUDIT LOG]:', JSON.stringify({
|
||||
timestamp: new Date(),
|
||||
...auditData
|
||||
}, null, 2));
|
||||
return true;
|
||||
// Only allow the middleware to write to the DB (which passes method/ip) to prevent duplicate entries
|
||||
if (!auditData.details || !auditData.details.method) {
|
||||
return true; // silently skip service-level calls
|
||||
}
|
||||
|
||||
try {
|
||||
await AuditLogRepository.create({
|
||||
action: auditData.action,
|
||||
resource: auditData.resource,
|
||||
resource_id: auditData.resourceId?.toString() || null,
|
||||
user_id: (auditData.userId && auditData.userId !== 'anonymous' && auditData.userId !== 'system')
|
||||
? parseInt(auditData.userId, 10) || null
|
||||
: null,
|
||||
old_value: auditData.old_value || null,
|
||||
new_value: auditData.new_value || null,
|
||||
details: auditData.details || null,
|
||||
status: auditData.status || 'success'
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('[AuditService] Failed to create audit log:', err.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
import { SocketService } from './socket.service.js';
|
||||
|
||||
export class NotificationService {
|
||||
static async send(userId, notification) {
|
||||
console.log(`Sending notification to user ${userId}:`, notification);
|
||||
|
||||
// Broadcast websocket notification
|
||||
SocketService.to(`user:${userId}`, 'notification:received', notification);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static async broadcast(notification) {
|
||||
console.log('Broadcasting notification:', notification);
|
||||
SocketService.broadcast('notification:broadcast', notification);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Shared notification service entry point.
|
||||
*
|
||||
* All notification generation is centralised in the feature-level NotificationService.
|
||||
* This module re-exports it so business services that import from the shared path
|
||||
* receive the same singleton instance.
|
||||
*
|
||||
* Business services must call notifyTenant() after a committed transaction.
|
||||
* They must never access NotificationRepository directly.
|
||||
*/
|
||||
export { default } from '../../features/notifications/notifications/notification.service.js';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Server } from 'socket.io';
|
||||
import { notificationEvents } from '../../features/notifications/notifications/notification.events.js';
|
||||
|
||||
class SocketServiceClass {
|
||||
constructor() {
|
||||
@@ -16,10 +17,38 @@ class SocketServiceClass {
|
||||
this.io.on('connection', (socket) => {
|
||||
console.log('Client connected to Socket.io: ' + socket.id);
|
||||
|
||||
const userId = socket.handshake.query.userId;
|
||||
const tenantId = socket.handshake.query.tenantId;
|
||||
|
||||
if (userId) {
|
||||
socket.join(`user_${userId}`);
|
||||
console.log(`Socket ${socket.id} joined room: user_${userId}`);
|
||||
}
|
||||
if (tenantId) {
|
||||
socket.join(`tenant_${tenantId}`);
|
||||
console.log(`Socket ${socket.id} joined room: tenant_${tenantId}`);
|
||||
}
|
||||
|
||||
socket.on('join_user', (uid) => {
|
||||
if (uid) {
|
||||
socket.join(`user_${uid}`);
|
||||
console.log(`Socket ${socket.id} joined room: user_${uid} via event`);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('Client disconnected: ' + socket.id);
|
||||
});
|
||||
});
|
||||
|
||||
// Register decoupled event listeners for real-time notifications
|
||||
notificationEvents.on('notification:created', ({ recipientUserId, notification }) => {
|
||||
this.to(`user_${recipientUserId}`, 'notification:created', notification);
|
||||
});
|
||||
|
||||
notificationEvents.on('notification:unread-count', ({ recipientUserId, count }) => {
|
||||
this.to(`user_${recipientUserId}`, 'notification:unread-count', { unreadCount: count });
|
||||
});
|
||||
}
|
||||
|
||||
broadcast(event, data) {
|
||||
|
||||
Reference in New Issue
Block a user