Implemented notifications.
Some checks failed
farmcontrol/farmcontrol-api/pipeline/head There was a failure building this commit
Some checks failed
farmcontrol/farmcontrol-api/pipeline/head There was a failure building this commit
This commit is contained in:
parent
838e48ade6
commit
3e47cb131b
@ -1,6 +1,7 @@
|
||||
import config from '../config.js';
|
||||
import { fileModel } from './schemas/management/file.schema.js';
|
||||
import _ from 'lodash';
|
||||
import log4js from 'log4js';
|
||||
import {
|
||||
deleteAuditLog,
|
||||
distributeDelete,
|
||||
@ -8,9 +9,6 @@ import {
|
||||
modelHasRef,
|
||||
getFieldsByRef,
|
||||
getQueryToCacheKey,
|
||||
} from '../utils.js';
|
||||
import log4js from 'log4js';
|
||||
import {
|
||||
editAuditLog,
|
||||
distributeUpdate,
|
||||
newAuditLog,
|
||||
@ -19,6 +17,8 @@ import {
|
||||
distributeChildDelete,
|
||||
distributeChildNew,
|
||||
distributeStats,
|
||||
editNotification,
|
||||
deleteNotification,
|
||||
} from '../utils.js';
|
||||
import { getAllModels } from '../services/misc/model.js';
|
||||
import { redisServer } from './redis.js';
|
||||
@ -802,6 +802,20 @@ export const editObject = async ({ model, id, updateData, user, populate, recalc
|
||||
parentType,
|
||||
user
|
||||
);
|
||||
|
||||
if (
|
||||
parentType !== 'notification' &&
|
||||
parentType !== 'auditLog' &&
|
||||
parentType !== 'userNotifier'
|
||||
) {
|
||||
await editNotification(
|
||||
previousExpandedObject,
|
||||
{ ...previousExpandedObject, ...updateData },
|
||||
id,
|
||||
parentType,
|
||||
user
|
||||
);
|
||||
}
|
||||
// Distribute update
|
||||
await distributeUpdate(updateData, id, parentType);
|
||||
// Call childUpdate event for any child objects
|
||||
@ -878,6 +892,7 @@ export const newObject = async ({ model, newData, user = null }, distributeChang
|
||||
const created = expandObjectIds(result.toObject());
|
||||
|
||||
await newAuditLog(newData, created._id, parentType, user);
|
||||
|
||||
if (distributeChanges == true) {
|
||||
await distributeNew(created, parentType);
|
||||
}
|
||||
@ -922,7 +937,15 @@ export const deleteObject = async ({ model, id, user = null }, distributeChanges
|
||||
|
||||
const deleted = expandObjectIds(result.toObject());
|
||||
// Audit log the deletion
|
||||
await deleteAuditLog(deleted, id, parentType, user, 'delete');
|
||||
await deleteAuditLog(deleted, id, parentType, user);
|
||||
|
||||
if (
|
||||
parentType !== 'notification' &&
|
||||
parentType !== 'auditLog' &&
|
||||
parentType !== 'userNotifier'
|
||||
) {
|
||||
await deleteNotification(deleted, id, parentType, user);
|
||||
}
|
||||
|
||||
if (distributeChanges == true) {
|
||||
await distributeDelete(deleted, parentType);
|
||||
|
||||
51
src/database/schemas/misc/notification.schema.js
Normal file
51
src/database/schemas/misc/notification.schema.js
Normal file
@ -0,0 +1,51 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { generateId } from '../../utils.js';
|
||||
const { Schema } = mongoose;
|
||||
|
||||
const notificationSchema = new mongoose.Schema({
|
||||
_reference: { type: String, default: () => generateId()() },
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'user',
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: 'info',
|
||||
},
|
||||
read: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
metadata: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
createdAt: {
|
||||
type: Date,
|
||||
required: true,
|
||||
default: Date.now,
|
||||
},
|
||||
updatedAt: {
|
||||
type: Date,
|
||||
required: true,
|
||||
default: Date.now,
|
||||
},
|
||||
});
|
||||
|
||||
notificationSchema.virtual('id').get(function () {
|
||||
return this._id;
|
||||
});
|
||||
|
||||
notificationSchema.set('toJSON', { virtuals: true });
|
||||
|
||||
export const notificationModel = mongoose.model('notification', notificationSchema);
|
||||
44
src/database/schemas/misc/usernotifier.schema.js
Normal file
44
src/database/schemas/misc/usernotifier.schema.js
Normal file
@ -0,0 +1,44 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { generateId } from '../../utils.js';
|
||||
const { Schema } = mongoose;
|
||||
|
||||
const userNotifierSchema = new mongoose.Schema({
|
||||
_reference: { type: String, default: () => generateId()() },
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'user',
|
||||
required: true,
|
||||
},
|
||||
email: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false,
|
||||
},
|
||||
object: {
|
||||
type: Schema.Types.ObjectId,
|
||||
refPath: 'objectType',
|
||||
required: true,
|
||||
},
|
||||
objectType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
createdAt: {
|
||||
type: Date,
|
||||
required: true,
|
||||
default: Date.now,
|
||||
},
|
||||
updatedAt: {
|
||||
type: Date,
|
||||
required: true,
|
||||
default: Date.now,
|
||||
},
|
||||
});
|
||||
|
||||
userNotifierSchema.virtual('id').get(function () {
|
||||
return this._id;
|
||||
});
|
||||
|
||||
userNotifierSchema.set('toJSON', { virtuals: true });
|
||||
|
||||
export const userNotifierModel = mongoose.model('userNotifier', userNotifierSchema);
|
||||
@ -16,6 +16,8 @@ import { auditLogModel } from './management/auditlog.schema.js';
|
||||
import { userModel } from './management/user.schema.js';
|
||||
import { noteTypeModel } from './management/notetype.schema.js';
|
||||
import { noteModel } from './misc/note.schema.js';
|
||||
import { notificationModel } from './misc/notification.schema.js';
|
||||
import { userNotifierModel } from './misc/usernotifier.schema.js';
|
||||
import { documentSizeModel } from './management/documentsize.schema.js';
|
||||
import { documentTemplateModel } from './management/documenttemplate.schema.js';
|
||||
import { hostModel } from './management/host.schema.js';
|
||||
@ -33,77 +35,243 @@ import { salesOrderModel } from './sales/salesorder.schema.js';
|
||||
|
||||
// Map prefixes to models and id fields
|
||||
export const models = {
|
||||
PRN: { model: printerModel, idField: '_id', type: 'printer', referenceField: '_reference' },
|
||||
FIL: { model: filamentModel, idField: '_id', type: 'filament', referenceField: '_reference' },
|
||||
GCF: { model: gcodeFileModel, idField: '_id', type: 'gcodeFile', referenceField: '_reference' },
|
||||
JOB: { model: jobModel, idField: '_id', type: 'job', referenceField: '_reference' },
|
||||
PRT: { model: partModel, idField: '_id', type: 'part', referenceField: '_reference' },
|
||||
PRD: { model: productModel, idField: '_id', type: 'product', referenceField: '_reference' },
|
||||
VEN: { model: vendorModel, idField: '_id', type: 'vendor', referenceField: '_reference' },
|
||||
SJB: { model: subJobModel, idField: '_id', type: 'subJob', referenceField: '_reference' },
|
||||
PRN: {
|
||||
model: printerModel,
|
||||
idField: '_id',
|
||||
type: 'printer',
|
||||
referenceField: '_reference',
|
||||
label: 'Printer',
|
||||
},
|
||||
FIL: {
|
||||
model: filamentModel,
|
||||
idField: '_id',
|
||||
type: 'filament',
|
||||
referenceField: '_reference',
|
||||
label: 'Filament',
|
||||
},
|
||||
GCF: {
|
||||
model: gcodeFileModel,
|
||||
idField: '_id',
|
||||
type: 'gcodeFile',
|
||||
referenceField: '_reference',
|
||||
label: 'G-Code File',
|
||||
},
|
||||
JOB: { model: jobModel, idField: '_id', type: 'job', referenceField: '_reference', label: 'Job' },
|
||||
PRT: {
|
||||
model: partModel,
|
||||
idField: '_id',
|
||||
type: 'part',
|
||||
referenceField: '_reference',
|
||||
label: 'Part',
|
||||
},
|
||||
PRD: {
|
||||
model: productModel,
|
||||
idField: '_id',
|
||||
type: 'product',
|
||||
referenceField: '_reference',
|
||||
label: 'Product',
|
||||
},
|
||||
VEN: {
|
||||
model: vendorModel,
|
||||
idField: '_id',
|
||||
type: 'vendor',
|
||||
referenceField: '_reference',
|
||||
label: 'Vendor',
|
||||
},
|
||||
SJB: {
|
||||
model: subJobModel,
|
||||
idField: '_id',
|
||||
type: 'subJob',
|
||||
referenceField: '_reference',
|
||||
label: 'Sub Job',
|
||||
},
|
||||
FLS: {
|
||||
model: filamentStockModel,
|
||||
idField: '_id',
|
||||
type: 'filamentStock',
|
||||
referenceField: '_reference',
|
||||
label: 'Filament Stock',
|
||||
},
|
||||
SEV: {
|
||||
model: stockEventModel,
|
||||
idField: '_id',
|
||||
type: 'stockEvent',
|
||||
referenceField: '_reference',
|
||||
label: 'Stock Event',
|
||||
},
|
||||
SAU: {
|
||||
model: stockAuditModel,
|
||||
idField: '_id',
|
||||
type: 'stockAudit',
|
||||
referenceField: '_reference',
|
||||
label: 'Stock Audit',
|
||||
},
|
||||
PTS: {
|
||||
model: partStockModel,
|
||||
idField: '_id',
|
||||
type: 'partStock',
|
||||
referenceField: '_reference',
|
||||
label: 'Part Stock',
|
||||
},
|
||||
PDS: {
|
||||
model: null,
|
||||
idField: '_id',
|
||||
type: 'productStock',
|
||||
referenceField: '_reference',
|
||||
label: 'Product Stock',
|
||||
}, // No productStockModel found
|
||||
ADL: {
|
||||
model: auditLogModel,
|
||||
idField: '_id',
|
||||
type: 'auditLog',
|
||||
referenceField: '_reference',
|
||||
label: 'Audit Log',
|
||||
},
|
||||
USR: {
|
||||
model: userModel,
|
||||
idField: '_id',
|
||||
type: 'user',
|
||||
referenceField: '_reference',
|
||||
label: 'User',
|
||||
},
|
||||
NTY: {
|
||||
model: noteTypeModel,
|
||||
idField: '_id',
|
||||
type: 'noteType',
|
||||
referenceField: '_reference',
|
||||
label: 'Note Type',
|
||||
},
|
||||
NTE: {
|
||||
model: noteModel,
|
||||
idField: '_id',
|
||||
type: 'note',
|
||||
referenceField: '_reference',
|
||||
label: 'Note',
|
||||
},
|
||||
NTF: {
|
||||
model: notificationModel,
|
||||
idField: '_id',
|
||||
type: 'notification',
|
||||
label: 'Notification',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
ONF: {
|
||||
model: userNotifierModel,
|
||||
idField: '_id',
|
||||
type: 'userNotifier',
|
||||
label: 'User Notifier',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
SEV: { model: stockEventModel, idField: '_id', type: 'stockEvent', referenceField: '_reference' },
|
||||
SAU: { model: stockAuditModel, idField: '_id', type: 'stockAudit', referenceField: '_reference' },
|
||||
PTS: { model: partStockModel, idField: '_id', type: 'partStock', referenceField: '_reference' },
|
||||
PDS: { model: null, idField: '_id', type: 'productStock', referenceField: '_reference' }, // No productStockModel found
|
||||
ADL: { model: auditLogModel, idField: '_id', type: 'auditLog', referenceField: '_reference' },
|
||||
USR: { model: userModel, idField: '_id', type: 'user', referenceField: '_reference' },
|
||||
NTY: { model: noteTypeModel, idField: '_id', type: 'noteType', referenceField: '_reference' },
|
||||
NTE: { model: noteModel, idField: '_id', type: 'note', referenceField: '_reference' },
|
||||
DSZ: {
|
||||
model: documentSizeModel,
|
||||
idField: '_id',
|
||||
type: 'documentSize',
|
||||
label: 'Document Size',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
DTP: {
|
||||
model: documentTemplateModel,
|
||||
idField: '_id',
|
||||
type: 'documentTemplate',
|
||||
label: 'Document Template',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
DPR: {
|
||||
model: documentPrinterModel,
|
||||
idField: '_id',
|
||||
type: 'documentPrinter',
|
||||
label: 'Document Printer',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
DJB: {
|
||||
model: documentJobModel,
|
||||
idField: '_id',
|
||||
type: 'documentJob',
|
||||
label: 'Document Job',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
HST: { model: hostModel, idField: '_id', type: 'host', referenceField: '_reference' },
|
||||
FLE: { model: fileModel, idField: '_id', type: 'file', referenceField: '_reference' },
|
||||
HST: {
|
||||
model: hostModel,
|
||||
idField: '_id',
|
||||
type: 'host',
|
||||
referenceField: '_reference',
|
||||
label: 'Host',
|
||||
},
|
||||
FLE: {
|
||||
model: fileModel,
|
||||
idField: '_id',
|
||||
type: 'file',
|
||||
referenceField: '_reference',
|
||||
label: 'File',
|
||||
},
|
||||
POR: {
|
||||
model: purchaseOrderModel,
|
||||
idField: '_id',
|
||||
type: 'purchaseOrder',
|
||||
label: 'Purchase Order',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
ODI: {
|
||||
model: orderItemModel,
|
||||
idField: '_id',
|
||||
type: 'orderItem',
|
||||
label: 'Order Item',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
COS: {
|
||||
model: courierServiceModel,
|
||||
idField: '_id',
|
||||
type: 'courierService',
|
||||
label: 'Courier Service',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
COR: {
|
||||
model: courierModel,
|
||||
idField: '_id',
|
||||
type: 'courier',
|
||||
label: 'Courier',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
TXR: {
|
||||
model: taxRateModel,
|
||||
idField: '_id',
|
||||
type: 'taxRate',
|
||||
label: 'Tax Rate',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
TXD: {
|
||||
model: taxRecordModel,
|
||||
idField: '_id',
|
||||
type: 'taxRecord',
|
||||
label: 'Tax Record',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
SHP: {
|
||||
model: shipmentModel,
|
||||
idField: '_id',
|
||||
type: 'shipment',
|
||||
label: 'Shipment',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
INV: {
|
||||
model: invoiceModel,
|
||||
idField: '_id',
|
||||
type: 'invoice',
|
||||
label: 'Invoice',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
CLI: {
|
||||
model: clientModel,
|
||||
idField: '_id',
|
||||
type: 'client',
|
||||
label: 'Client',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
SOR: {
|
||||
model: salesOrderModel,
|
||||
idField: '_id',
|
||||
type: 'salesOrder',
|
||||
label: 'Sales Order',
|
||||
referenceField: '_reference',
|
||||
},
|
||||
COR: { model: courierModel, idField: '_id', type: 'courier', referenceField: '_reference' },
|
||||
TXR: { model: taxRateModel, idField: '_id', type: 'taxRate', referenceField: '_reference' },
|
||||
TXD: { model: taxRecordModel, idField: '_id', type: 'taxRecord', referenceField: '_reference' },
|
||||
SHP: { model: shipmentModel, idField: '_id', type: 'shipment', referenceField: '_reference' },
|
||||
INV: { model: invoiceModel, idField: '_id', type: 'invoice', referenceField: '_reference' },
|
||||
CLI: { model: clientModel, idField: '_id', type: 'client', referenceField: '_reference' },
|
||||
SOR: { model: salesOrderModel, idField: '_id', type: 'salesOrder', referenceField: '_reference' },
|
||||
};
|
||||
|
||||
@ -41,6 +41,8 @@ import {
|
||||
paymentRoutes,
|
||||
clientRoutes,
|
||||
salesOrderRoutes,
|
||||
userNotifierRoutes,
|
||||
notificationRoutes,
|
||||
} from './routes/index.js';
|
||||
import path from 'path';
|
||||
import * as fs from 'fs';
|
||||
@ -148,6 +150,8 @@ app.use('/payments', paymentRoutes);
|
||||
app.use('/clients', clientRoutes);
|
||||
app.use('/salesorders', salesOrderRoutes);
|
||||
app.use('/notes', noteRoutes);
|
||||
app.use('/usernotifiers', userNotifierRoutes);
|
||||
app.use('/notifications', notificationRoutes);
|
||||
|
||||
// Start the application
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
|
||||
@ -34,6 +34,8 @@ import paymentRoutes from './finance/payments.js';
|
||||
import clientRoutes from './sales/clients.js';
|
||||
import salesOrderRoutes from './sales/salesorders.js';
|
||||
import noteRoutes from './misc/notes.js';
|
||||
import userNotifierRoutes from './misc/usernotifiers.js';
|
||||
import notificationRoutes from './misc/notifications.js';
|
||||
|
||||
export {
|
||||
userRoutes,
|
||||
@ -72,4 +74,6 @@ export {
|
||||
paymentRoutes,
|
||||
clientRoutes,
|
||||
salesOrderRoutes,
|
||||
userNotifierRoutes,
|
||||
notificationRoutes,
|
||||
};
|
||||
|
||||
37
src/routes/misc/notifications.js
Normal file
37
src/routes/misc/notifications.js
Normal file
@ -0,0 +1,37 @@
|
||||
import express from 'express';
|
||||
import { isAuthenticated } from '../../keycloak.js';
|
||||
import {
|
||||
listNotificationsRouteHandler,
|
||||
markNotificationAsReadRouteHandler,
|
||||
markAllNotificationsAsReadRouteHandler,
|
||||
deleteNotificationRouteHandler,
|
||||
deleteAllNotificationsRouteHandler,
|
||||
} from '../../services/misc/notifications.js';
|
||||
import { getFilter } from '../../utils.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', isAuthenticated, (req, res) => {
|
||||
const { page = 1, limit = 50, sort = 'createdAt', order = 'descend' } = req.query;
|
||||
const allowedFilters = ['user'];
|
||||
const filter = getFilter(req.query, allowedFilters);
|
||||
listNotificationsRouteHandler(req, res, page, limit, filter, sort, order);
|
||||
});
|
||||
|
||||
router.put('/read-all', isAuthenticated, (req, res) => {
|
||||
markAllNotificationsAsReadRouteHandler(req, res);
|
||||
});
|
||||
|
||||
router.put('/:id/read', isAuthenticated, (req, res) => {
|
||||
markNotificationAsReadRouteHandler(req, res);
|
||||
});
|
||||
|
||||
router.delete('/', isAuthenticated, (req, res) => {
|
||||
deleteAllNotificationsRouteHandler(req, res);
|
||||
});
|
||||
|
||||
router.delete('/:id', isAuthenticated, (req, res) => {
|
||||
deleteNotificationRouteHandler(req, res);
|
||||
});
|
||||
|
||||
export default router;
|
||||
37
src/routes/misc/usernotifiers.js
Normal file
37
src/routes/misc/usernotifiers.js
Normal file
@ -0,0 +1,37 @@
|
||||
import express from 'express';
|
||||
import { isAuthenticated } from '../../keycloak.js';
|
||||
import {
|
||||
listUserNotifiersRouteHandler,
|
||||
getUserNotifierRouteHandler,
|
||||
newUserNotifierRouteHandler,
|
||||
deleteUserNotifierRouteHandler,
|
||||
editUserNotifierRouteHandler,
|
||||
} from '../../services/misc/usernotifiers.js';
|
||||
import { getFilter } from '../../utils.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', isAuthenticated, (req, res) => {
|
||||
const { page, limit, property, search, sort, order } = req.query;
|
||||
const allowedFilters = ['user', 'object', 'objectType'];
|
||||
const filter = getFilter(req.query, allowedFilters);
|
||||
listUserNotifiersRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
||||
});
|
||||
|
||||
router.post('/', isAuthenticated, (req, res) => {
|
||||
newUserNotifierRouteHandler(req, res);
|
||||
});
|
||||
|
||||
router.get('/:id', isAuthenticated, (req, res) => {
|
||||
getUserNotifierRouteHandler(req, res);
|
||||
});
|
||||
|
||||
router.put('/:id', isAuthenticated, (req, res) => {
|
||||
editUserNotifierRouteHandler(req, res);
|
||||
});
|
||||
|
||||
router.delete('/:id', isAuthenticated, (req, res) => {
|
||||
deleteUserNotifierRouteHandler(req, res);
|
||||
});
|
||||
|
||||
export default router;
|
||||
105
src/services/misc/notifications.js
Normal file
105
src/services/misc/notifications.js
Normal file
@ -0,0 +1,105 @@
|
||||
import config from '../../config.js';
|
||||
import { notificationModel } from '../../database/schemas/misc/notification.schema.js';
|
||||
import log4js from 'log4js';
|
||||
import { listObjects, editObject } from '../../database/database.js';
|
||||
|
||||
const logger = log4js.getLogger('Notifications');
|
||||
logger.level = config.server.logLevel;
|
||||
|
||||
export const listNotificationsRouteHandler = async (
|
||||
req,
|
||||
res,
|
||||
page = 1,
|
||||
limit = 50,
|
||||
filter = {},
|
||||
sort = 'createdAt',
|
||||
order = 'descend'
|
||||
) => {
|
||||
const userFilter = { ...filter, user: req.user._id };
|
||||
const result = await listObjects({
|
||||
model: notificationModel,
|
||||
page,
|
||||
limit,
|
||||
filter: userFilter,
|
||||
sort,
|
||||
order,
|
||||
populate: [],
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
logger.error('Error listing notifications.');
|
||||
res.status(result.code || 500).send(result);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(`List of notifications (Page ${page}, Limit ${limit}). Count: ${result.length}`);
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
export const markNotificationAsReadRouteHandler = async (req, res) => {
|
||||
const id = req.params.id;
|
||||
|
||||
const existing = await notificationModel.findById(id).lean();
|
||||
if (!existing) {
|
||||
return res.status(404).send({ error: 'Notification not found' });
|
||||
}
|
||||
if (String(existing.user) !== String(req.user._id)) {
|
||||
return res.status(403).send({ error: 'Forbidden: you can only update your own notifications' });
|
||||
}
|
||||
|
||||
const result = await editObject({
|
||||
model: notificationModel,
|
||||
id,
|
||||
updateData: { read: true, updatedAt: new Date() },
|
||||
user: req.user,
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
logger.error('Error marking notification as read:', result.error);
|
||||
return res.status(result.code || 500).send(result);
|
||||
}
|
||||
|
||||
logger.debug(`Marked notification as read: ${id}`);
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
export const markAllNotificationsAsReadRouteHandler = async (req, res) => {
|
||||
try {
|
||||
await notificationModel.updateMany(
|
||||
{ user: req.user._id, read: false },
|
||||
{ $set: { read: true, updatedAt: new Date() } }
|
||||
);
|
||||
logger.debug('Marked all notifications as read');
|
||||
res.send({ status: 'ok' });
|
||||
} catch (err) {
|
||||
logger.error('Error marking all notifications as read:', err);
|
||||
res.status(500).send({ error: 'Failed to mark all as read' });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteNotificationRouteHandler = async (req, res) => {
|
||||
const id = req.params.id;
|
||||
|
||||
const existing = await notificationModel.findById(id).lean();
|
||||
if (!existing) {
|
||||
return res.status(404).send({ error: 'Notification not found' });
|
||||
}
|
||||
if (String(existing.user._id) !== String(req.user._id)) {
|
||||
return res.status(403).send({ error: 'Forbidden: you can only delete your own notifications' });
|
||||
}
|
||||
|
||||
await notificationModel.findByIdAndDelete(id);
|
||||
logger.debug('Deleted notification:', id);
|
||||
res.send({ status: 'ok' });
|
||||
};
|
||||
|
||||
export const deleteAllNotificationsRouteHandler = async (req, res) => {
|
||||
try {
|
||||
await notificationModel.deleteMany({ user: req.user._id });
|
||||
logger.debug('Deleted all notifications');
|
||||
res.send({ status: 'ok' });
|
||||
} catch (err) {
|
||||
logger.error('Error deleting all notifications:', err);
|
||||
res.status(500).send({ error: 'Failed to delete all notifications' });
|
||||
}
|
||||
};
|
||||
146
src/services/misc/usernotifiers.js
Normal file
146
src/services/misc/usernotifiers.js
Normal file
@ -0,0 +1,146 @@
|
||||
import config from '../../config.js';
|
||||
import { userNotifierModel } from '../../database/schemas/misc/usernotifier.schema.js';
|
||||
import log4js from 'log4js';
|
||||
import {
|
||||
deleteObject,
|
||||
editObject,
|
||||
getObject,
|
||||
listObjects,
|
||||
newObject,
|
||||
} from '../../database/database.js';
|
||||
|
||||
const logger = log4js.getLogger('UserNotifiers');
|
||||
logger.level = config.server.logLevel;
|
||||
|
||||
export const listUserNotifiersRouteHandler = async (
|
||||
req,
|
||||
res,
|
||||
page = 1,
|
||||
limit = 25,
|
||||
property = '',
|
||||
filter = {},
|
||||
search = '',
|
||||
sort = '',
|
||||
order = 'ascend'
|
||||
) => {
|
||||
const result = await listObjects({
|
||||
model: userNotifierModel,
|
||||
page,
|
||||
limit,
|
||||
property,
|
||||
filter,
|
||||
search,
|
||||
sort,
|
||||
order,
|
||||
populate: ['user', 'object'],
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
logger.error('Error listing user notifiers.');
|
||||
res.status(result.code).send(result);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(`List of user notifiers (Page ${page}, Limit ${limit}). Count: ${result.length}`);
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
export const getUserNotifierRouteHandler = async (req, res) => {
|
||||
const id = req.params.id;
|
||||
const result = await getObject({
|
||||
model: userNotifierModel,
|
||||
id,
|
||||
populate: ['user', 'object'],
|
||||
});
|
||||
if (result?.error) {
|
||||
logger.warn(`User notifier not found with supplied id.`);
|
||||
return res.status(result.code).send(result);
|
||||
}
|
||||
logger.debug(`Retrieved user notifier with ID: ${id}`);
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
export const newUserNotifierRouteHandler = async (req, res) => {
|
||||
const newData = {
|
||||
user: req.user._id,
|
||||
object: req.body.object,
|
||||
objectType: req.body.objectType,
|
||||
};
|
||||
const result = await newObject({
|
||||
model: userNotifierModel,
|
||||
newData,
|
||||
user: req.user,
|
||||
});
|
||||
if (result.error) {
|
||||
logger.error('No user notifier created:', result.error);
|
||||
return res.status(result.code).send(result);
|
||||
}
|
||||
|
||||
logger.debug(`New user notifier with ID: ${result._id}`);
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
export const editUserNotifierRouteHandler = async (req, res) => {
|
||||
const id = req.params.id;
|
||||
|
||||
const existing = await getObject({
|
||||
model: userNotifierModel,
|
||||
id,
|
||||
});
|
||||
if (existing?.error) {
|
||||
return res.status(existing.code).send(existing);
|
||||
}
|
||||
if (String(existing.user._id) !== String(req.user._id)) {
|
||||
return res.status(403).send({ error: 'Forbidden: you can only edit your own notifiers' });
|
||||
}
|
||||
|
||||
const updateData = {
|
||||
updatedAt: new Date(),
|
||||
email: req.body.email,
|
||||
};
|
||||
const result = await editObject({
|
||||
model: userNotifierModel,
|
||||
id,
|
||||
updateData,
|
||||
user: req.user,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
logger.error('Error editing user notifier:', result.error);
|
||||
return res.status(result.code).send(result);
|
||||
}
|
||||
|
||||
logger.debug(`Edited user notifier with ID: ${id}`);
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
export const deleteUserNotifierRouteHandler = async (req, res) => {
|
||||
const id = req.params.id;
|
||||
|
||||
const existing = await getObject({
|
||||
model: userNotifierModel,
|
||||
id,
|
||||
});
|
||||
if (existing?.error) {
|
||||
return res.status(existing.code).send(existing);
|
||||
}
|
||||
if (String(existing.user._id) !== String(req.user._id)) {
|
||||
return res.status(403).send({ error: 'Forbidden: you can only delete your own notifiers' });
|
||||
}
|
||||
|
||||
const result = await deleteObject({
|
||||
model: userNotifierModel,
|
||||
id,
|
||||
user: req.user,
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
logger.error('No user notifier deleted:', result.error);
|
||||
return res.status(result.code).send(result);
|
||||
}
|
||||
|
||||
logger.info(`Successfully deleted user notifier ${id}`);
|
||||
res.send({
|
||||
status: 'ok',
|
||||
});
|
||||
};
|
||||
79
src/utils.js
79
src/utils.js
@ -1,11 +1,14 @@
|
||||
import { mongoose } from 'mongoose';
|
||||
import { auditLogModel } from './database/schemas/management/auditlog.schema.js';
|
||||
import { notificationModel } from './database/schemas/misc/notification.schema.js';
|
||||
import { userNotifierModel } from './database/schemas/misc/usernotifier.schema.js';
|
||||
import exifr from 'exifr';
|
||||
import { natsServer } from './database/nats.js';
|
||||
import log4js from 'log4js';
|
||||
import config from './config.js';
|
||||
import crypto from 'crypto';
|
||||
import canonicalize from 'canonical-json';
|
||||
import { getModelByName } from './services/misc/model.js';
|
||||
|
||||
const logger = log4js.getLogger('Utils');
|
||||
logger.level = config.server.logLevel;
|
||||
@ -350,7 +353,11 @@ function getChangedValues(oldObj, newObj, old = false) {
|
||||
return changes;
|
||||
}
|
||||
|
||||
const AUDIT_EXCLUDED_MODELS = ['notification', 'userNotifier'];
|
||||
|
||||
async function newAuditLog(newValue, parentId, parentType, user) {
|
||||
if (AUDIT_EXCLUDED_MODELS.includes(parentType)) return;
|
||||
|
||||
// Filter out createdAt and updatedAt from newValue
|
||||
const filteredNewValue = { ...newValue };
|
||||
delete filteredNewValue.createdAt;
|
||||
@ -372,6 +379,8 @@ async function newAuditLog(newValue, parentId, parentType, user) {
|
||||
}
|
||||
|
||||
async function editAuditLog(oldValue, newValue, parentId, parentType, user) {
|
||||
if (AUDIT_EXCLUDED_MODELS.includes(parentType)) return;
|
||||
|
||||
// Get only the changed values
|
||||
const changedOldValues = getChangedValues(oldValue, newValue, true);
|
||||
const changedNewValues = getChangedValues(oldValue, newValue, false);
|
||||
@ -398,7 +407,35 @@ async function editAuditLog(oldValue, newValue, parentId, parentType, user) {
|
||||
await distributeNew(auditLog._id, 'auditLog');
|
||||
}
|
||||
|
||||
async function editNotification(oldValue, newValue, parentId, parentType, user) {
|
||||
const model = getModelByName(parentType);
|
||||
const objectName = oldValue?.name ?? newValue?.name ?? model?.label ?? parentType;
|
||||
const changedOldValues = getChangedValues(oldValue, newValue, true);
|
||||
const changedNewValues = getChangedValues(oldValue, newValue, false);
|
||||
|
||||
if (Object.keys(changedOldValues).length === 0 || Object.keys(changedNewValues).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await notfiyObjectUserNotifiers(
|
||||
parentId,
|
||||
parentType,
|
||||
`${objectName} edited by ${user?.firstName ?? 'unknown'} ${user?.lastName ?? ''}`,
|
||||
`The ${parentType} ${parentId} has been updated.`,
|
||||
'editObject',
|
||||
{
|
||||
old: changedOldValues,
|
||||
new: changedNewValues,
|
||||
objectType: parentType,
|
||||
object: { _id: parentId },
|
||||
user: { _id: user._id, firstName: user.firstName, lastName: user.lastName },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function deleteAuditLog(deleteValue, parentId, parentType, user) {
|
||||
if (AUDIT_EXCLUDED_MODELS.includes(parentType)) return;
|
||||
|
||||
const auditLog = new auditLogModel({
|
||||
changes: {
|
||||
old: deleteValue,
|
||||
@ -415,6 +452,24 @@ async function deleteAuditLog(deleteValue, parentId, parentType, user) {
|
||||
await distributeNew(auditLog._id, 'auditLog');
|
||||
}
|
||||
|
||||
async function deleteNotification(object, parentId, parentType, user) {
|
||||
const model = getModelByName(parentType);
|
||||
const objectName = oldValue?.name ?? newValue?.name ?? model?.label ?? parentType;
|
||||
await notfiyObjectUserNotifiers(
|
||||
parentId,
|
||||
parentType,
|
||||
`${objectName} deleted by ${user?.firstName ?? 'unknown'} ${user?.lastName ?? ''}`,
|
||||
`The ${parentType} ${parentId} has been deleted.`,
|
||||
'deleteObject',
|
||||
{
|
||||
object: object,
|
||||
objectType: parentType,
|
||||
object: { _id: parentId },
|
||||
user: { _id: user._id, firstName: user.firstName, lastName: user.lastName },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function getAuditLogs(idOrIds) {
|
||||
if (Array.isArray(idOrIds)) {
|
||||
return auditLogModel.find({ parent: { $in: idOrIds } }).populate('owner');
|
||||
@ -493,6 +548,27 @@ async function distributeChildNew(value, id, model) {
|
||||
}
|
||||
}
|
||||
|
||||
async function notfiyObjectUserNotifiers(id, objectType, title, message, type = 'info', metadata) {
|
||||
const userNotifiers = await userNotifierModel.find({ object: id, objectType: objectType });
|
||||
for (const userNotifier of userNotifiers) {
|
||||
await createNotification(userNotifier.user._id, title, message, type, metadata);
|
||||
}
|
||||
}
|
||||
|
||||
async function createNotification(user, title, message, type = 'info', metadata) {
|
||||
const notification = new notificationModel({
|
||||
user,
|
||||
title,
|
||||
message,
|
||||
type,
|
||||
metadata,
|
||||
});
|
||||
await notification.save();
|
||||
const value = notification.toJSON ? notification.toJSON() : notification;
|
||||
await natsServer.publish(`notifications.${user._id}`, value);
|
||||
return notification;
|
||||
}
|
||||
|
||||
function flatternObjectIds(object) {
|
||||
if (!object || typeof object !== 'object') {
|
||||
return object;
|
||||
@ -707,6 +783,8 @@ export {
|
||||
parseFilter,
|
||||
convertToCamelCase,
|
||||
newAuditLog,
|
||||
editNotification,
|
||||
deleteNotification,
|
||||
editAuditLog,
|
||||
deleteAuditLog,
|
||||
getAuditLogs,
|
||||
@ -719,6 +797,7 @@ export {
|
||||
distributeChildUpdate,
|
||||
distributeChildDelete,
|
||||
distributeChildNew,
|
||||
notfiyObjectUserNotifiers,
|
||||
getFilter, // <-- add here
|
||||
convertPropertiesString,
|
||||
getFileMeta,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user