diff --git a/src/database/database.js b/src/database/database.js index ed789ca..31c606b 100644 --- a/src/database/database.js +++ b/src/database/database.js @@ -1242,7 +1242,8 @@ export const editObject = async ({ model, id, updateData, user, populate, recalc if ( parentType !== 'notification' && parentType !== 'auditLog' && - parentType !== 'userNotifier' + parentType !== 'userNotifier' && + parentType !== 'objectView' ) { await editNotification(previousExpandedObject, updatedObject, id, parentType, user); } @@ -1600,7 +1601,8 @@ export const deleteObject = async ( if ( parentType !== 'notification' && parentType !== 'auditLog' && - parentType !== 'userNotifier' + parentType !== 'userNotifier' && + parentType !== 'objectView' ) { await deleteNotification(deleted, id.toString(), parentType, user); } diff --git a/src/database/schemas/management/documentprinter.schema.js b/src/database/schemas/management/documentprinter.schema.js index 09d9be0..70f1325 100644 --- a/src/database/schemas/management/documentprinter.schema.js +++ b/src/database/schemas/management/documentprinter.schema.js @@ -8,6 +8,8 @@ const connectionSchema = new Schema( protocol: { type: String, required: true }, host: { type: String, required: true }, port: { type: Number, required: false }, + username: { type: String, required: false }, + password: { type: String, required: false }, }, { _id: false } ); @@ -22,6 +24,8 @@ const documentPrinterSchema = new Schema( }, connection: { type: connectionSchema, required: true }, currentDocumentSize: { type: Schema.Types.ObjectId, ref: 'documentSize', required: false }, + supportedDocumentSizes: [{ type: Schema.Types.ObjectId, ref: 'documentSize', required: false }], + rotateOrientation: { type: Boolean, required: false, default: false }, tags: [{ type: String }], online: { type: Boolean, required: true, default: false }, active: { type: Boolean, required: true, default: true }, @@ -30,6 +34,10 @@ const documentPrinterSchema = new Schema( message: { type: String, required: false }, progress: { type: Number, required: false }, }, + paperState: { + type: { type: String, required: true, default: 'unknown' }, + message: { type: String, required: false }, + }, connectedAt: { type: Date, default: null }, host: { type: Schema.Types.ObjectId, ref: 'host', required: true }, vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: false }, diff --git a/src/database/schemas/misc/objectview.schema.js b/src/database/schemas/misc/objectview.schema.js new file mode 100644 index 0000000..4a99201 --- /dev/null +++ b/src/database/schemas/misc/objectview.schema.js @@ -0,0 +1,58 @@ +import mongoose from 'mongoose'; +const { Schema } = mongoose; + +const objectViewSchema = new mongoose.Schema({ + user: { + type: Schema.Types.ObjectId, + ref: 'user', + required: true, + }, + objectType: { + type: String, + required: true, + }, + name: { + type: String, + required: true, + }, + color: { + type: String, + required: true, + default: '#3498DB', + }, + private: { + type: Boolean, + required: true, + default: true, + }, + filter: { + type: Schema.Types.Mixed, + default: () => ({}), + }, + sort: { + type: Schema.Types.Mixed, + default: () => ({}), + }, + viewMode: { + type: Schema.Types.Mixed, + default: null, + }, + createdAt: { + type: Date, + required: true, + default: Date.now, + }, + updatedAt: { + type: Date, + required: true, + default: Date.now, + }, +}); + +objectViewSchema.virtual('id').get(function () { + return this._id; +}); + +objectViewSchema.set('toJSON', { virtuals: true }); + +export const objectViewModel = mongoose.model('objectView', objectViewSchema); diff --git a/src/database/schemas/models.js b/src/database/schemas/models.js index 2cb8d25..9c67b04 100644 --- a/src/database/schemas/models.js +++ b/src/database/schemas/models.js @@ -32,6 +32,7 @@ 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 { objectViewModel } from './misc/objectview.schema.js'; import { documentSizeModel } from './management/documentsize.schema.js'; import { documentTemplateModel } from './management/documenttemplate.schema.js'; import { hostModel } from './management/host.schema.js'; @@ -103,6 +104,7 @@ export const models = { NTE: modelEntry(() => noteModel, 'note', 'Note'), NTF: modelEntry(() => notificationModel, 'notification', 'Notification'), ONF: modelEntry(() => userNotifierModel, 'userNotifier', 'User Notifier'), + OVW: modelEntry(() => objectViewModel, 'objectView', 'Object View'), DSZ: modelEntry(() => documentSizeModel, 'documentSize', 'Document Size'), DTP: modelEntry(() => documentTemplateModel, 'documentTemplate', 'Document Template'), DPR: modelEntry(() => documentPrinterModel, 'documentPrinter', 'Document Printer'), diff --git a/src/index.js b/src/index.js index 9b1ba9d..1a0a62e 100644 --- a/src/index.js +++ b/src/index.js @@ -61,6 +61,7 @@ import { returnPolicyRoutes, paymentPolicyRoutes, userNotifierRoutes, + objectViewRoutes, notificationRoutes, odataRoutes, rssRoutes, @@ -230,6 +231,7 @@ app.use('/returnpolicies', returnPolicyRoutes); app.use('/paymentpolicies', paymentPolicyRoutes); app.use('/notes', noteRoutes); app.use('/usernotifiers', userNotifierRoutes); +app.use('/objectviews', objectViewRoutes); app.use('/notifications', notificationRoutes); app.use('/odata', odataRoutes); app.use('/rss', rssRoutes); diff --git a/src/routes/index.js b/src/routes/index.js index 0dff857..8637c31 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -54,6 +54,7 @@ import returnPolicyRoutes from './sales/returnpolicies.js'; import paymentPolicyRoutes from './finance/paymentpolicies.js'; import noteRoutes from './misc/notes.js'; import userNotifierRoutes from './misc/usernotifiers.js'; +import objectViewRoutes from './misc/objectviews.js'; import notificationRoutes from './misc/notifications.js'; import odataRoutes from './misc/odata.js'; import rssRoutes from './misc/rss.js'; @@ -121,6 +122,7 @@ export { returnPolicyRoutes, paymentPolicyRoutes, userNotifierRoutes, + objectViewRoutes, notificationRoutes, odataRoutes, rssRoutes, diff --git a/src/routes/misc/objectviews.js b/src/routes/misc/objectviews.js new file mode 100644 index 0000000..841ad41 --- /dev/null +++ b/src/routes/misc/objectviews.js @@ -0,0 +1,34 @@ +import express from 'express'; +import { isAuthenticated } from '../../keycloak.js'; +import { + listObjectViewsRouteHandler, + getObjectViewRouteHandler, + newObjectViewRouteHandler, + deleteObjectViewRouteHandler, + editObjectViewRouteHandler, +} from '../../services/misc/objectviews.js'; + +const router = express.Router(); + +router.get('/', isAuthenticated, async (req, res) => { + const { page, limit, objectType } = req.query; + listObjectViewsRouteHandler(req, res, page, limit, objectType); +}); + +router.post('/', isAuthenticated, async (req, res) => { + newObjectViewRouteHandler(req, res); +}); + +router.get('/:id', isAuthenticated, async (req, res) => { + getObjectViewRouteHandler(req, res); +}); + +router.put('/:id', isAuthenticated, async (req, res) => { + editObjectViewRouteHandler(req, res); +}); + +router.delete('/:id', isAuthenticated, async (req, res) => { + deleteObjectViewRouteHandler(req, res); +}); + +export default router; diff --git a/src/services/management/documentprinters.js b/src/services/management/documentprinters.js index 9146b5f..7fe5509 100644 --- a/src/services/management/documentprinters.js +++ b/src/services/management/documentprinters.js @@ -38,7 +38,7 @@ export const listDocumentPrintersRouteHandler = async ( search, sort, order, - populate: ['currentDocumentSize', 'host'], + populate: ['currentDocumentSize', 'supportedDocumentSizes', 'host'], }); if (result?.error) { @@ -65,7 +65,7 @@ export const listDocumentPrintersByPropertiesRouteHandler = async ( properties, filter, masterFilter, - populate: ['currentDocumentSize', 'host'], + populate: ['currentDocumentSize', 'supportedDocumentSizes', 'host'], }); if (result?.error) { @@ -106,7 +106,7 @@ export const getDocumentPrinterRouteHandler = async (req, res) => { const result = await getObject({ model: documentPrinterModel, id, - populate: ['currentDocumentSize', 'host'], + populate: ['currentDocumentSize', 'supportedDocumentSizes', 'host'], }); if (result?.error) { logger.warn(`Document Template not found with supplied id.`); @@ -129,6 +129,8 @@ export const editDocumentPrinterRouteHandler = async (req, res) => { active: req.body.active, connection: req.body.connection, currentDocumentSize: req.body.currentDocumentSize, + supportedDocumentSizes: req.body.supportedDocumentSizes, + rotateOrientation: req.body.rotateOrientation, host: req.body.host, vendor: req.body.vendor, }; @@ -138,7 +140,7 @@ export const editDocumentPrinterRouteHandler = async (req, res) => { id, updateData, user: req.user, - populate: ['currentDocumentSize', 'host'], + populate: ['currentDocumentSize', 'supportedDocumentSizes', 'host'], }); if (result.error) { @@ -160,6 +162,8 @@ export const newDocumentPrinterRouteHandler = async (req, res) => { active: req.body.active, connection: req.body.connection, currentDocumentSize: req.body.currentDocumentSize, + supportedDocumentSizes: req.body.supportedDocumentSizes, + rotateOrientation: req.body.rotateOrientation, host: req.body.host, vendor: req.body.vendor, }; diff --git a/src/services/misc/export.js b/src/services/misc/export.js index 77184d2..e0b8b45 100644 --- a/src/services/misc/export.js +++ b/src/services/misc/export.js @@ -8,6 +8,7 @@ export const EXPORT_FILTER_BY_TYPE = { note: ['parent._id', 'noteType', 'user'], notification: ['user'], userNotifier: ['user', 'object', 'objectType'], + objectView: ['user', 'objectType', 'private'], printer: ['host'], job: ['printer', 'gcodeFile'], subJob: ['job'], diff --git a/src/services/misc/objectviews.js b/src/services/misc/objectviews.js new file mode 100644 index 0000000..ab2e16e --- /dev/null +++ b/src/services/misc/objectviews.js @@ -0,0 +1,186 @@ +import config from '../../config.js'; +import { objectViewModel } from '../../database/schemas/misc/objectview.schema.js'; +import log4js from 'log4js'; +import { + deleteObject, + editObject, + getObject, + newObject, +} from '../../database/database.js'; + +const logger = log4js.getLogger('ObjectViews'); +logger.level = config.server.logLevel; + +const canModifyObjectView = (existing, user) => { + if (existing?.private === false) { + return true; + } + return String(existing?.user?._id ?? existing?.user) === String(user._id); +}; + +export const listObjectViewsRouteHandler = async ( + req, + res, + page = 1, + limit = 100, + objectType = '' +) => { + if (!objectType) { + return res.status(400).send({ error: 'objectType is required', code: 400 }); + } + + const query = { + objectType, + $or: [{ private: false }, { user: req.user._id }], + }; + + const skip = (Math.max(parseInt(page, 10) || 1, 1) - 1) * (parseInt(limit, 10) || 100); + const parsedLimit = parseInt(limit, 10) || 100; + + try { + const [items, totalCount] = await Promise.all([ + objectViewModel + .find(query) + .populate('user') + .sort({ createdAt: 1 }) + .skip(skip) + .limit(parsedLimit) + .lean({ virtuals: true }), + objectViewModel.countDocuments(query), + ]); + + logger.debug( + `List of object views for ${objectType} (Page ${page}, Limit ${parsedLimit}). Count: ${items.length}` + ); + res.set('X-Total-Count', String(totalCount)); + res.send(items); + } catch (error) { + logger.error('Error listing object views.', error); + res.status(500).send({ error: error.message, code: 500 }); + } +}; + +export const getObjectViewRouteHandler = async (req, res) => { + const id = req.params.id; + const result = await getObject({ + model: objectViewModel, + id, + populate: ['user'], + }); + if (result?.error) { + logger.warn(`Object view not found with supplied id.`); + return res.status(result.code).send(result); + } + + const isVisible = + result.private === false || + String(result.user?._id ?? result.user) === String(req.user._id); + if (!isVisible) { + return res.status(403).send({ error: 'Forbidden', code: 403 }); + } + + logger.debug(`Retrieved object view with ID: ${id}`); + res.send(result); +}; + +export const newObjectViewRouteHandler = async (req, res) => { + const newData = { + user: req.user._id, + objectType: req.body.objectType, + name: req.body.name || 'New View', + color: req.body.color || '#3498DB', + private: req.body.private !== false, + filter: req.body.filter || {}, + sort: req.body.sort || {}, + viewMode: req.body.viewMode ?? null, + }; + + if (!newData.objectType) { + return res.status(400).send({ error: 'objectType is required', code: 400 }); + } + + const result = await newObject({ + model: objectViewModel, + newData, + user: req.user, + }); + if (result.error) { + logger.error('No object view created:', result.error); + return res.status(result.code).send(result); + } + + logger.debug(`New object view with ID: ${result._id}`); + res.send(result); +}; + +export const editObjectViewRouteHandler = async (req, res) => { + const id = req.params.id; + + const existing = await getObject({ + model: objectViewModel, + id, + }); + if (existing?.error) { + return res.status(existing.code).send(existing); + } + if (!canModifyObjectView(existing, req.user)) { + return res.status(403).send({ error: 'Forbidden: you cannot edit this view', code: 403 }); + } + + const updateData = { + updatedAt: new Date(), + }; + if (req.body.name !== undefined) updateData.name = req.body.name; + if (req.body.color !== undefined) updateData.color = req.body.color; + if (req.body.private !== undefined) updateData.private = req.body.private; + if (req.body.filter !== undefined) updateData.filter = req.body.filter; + if (req.body.sort !== undefined) updateData.sort = req.body.sort; + if (req.body.viewMode !== undefined) updateData.viewMode = req.body.viewMode; + + const result = await editObject({ + model: objectViewModel, + id, + updateData, + user: req.user, + populate: ['user'], + }); + + if (result.error) { + logger.error('Error editing object view:', result.error); + return res.status(result.code).send(result); + } + + logger.debug(`Edited object view with ID: ${id}`); + res.send(result); +}; + +export const deleteObjectViewRouteHandler = async (req, res) => { + const id = req.params.id; + + const existing = await getObject({ + model: objectViewModel, + id, + }); + if (existing?.error) { + return res.status(existing.code).send(existing); + } + if (!canModifyObjectView(existing, req.user)) { + return res.status(403).send({ error: 'Forbidden: you cannot delete this view', code: 403 }); + } + + const result = await deleteObject({ + model: objectViewModel, + id, + user: req.user, + }); + + if (result?.error) { + logger.error('No object view deleted:', result.error); + return res.status(result.code).send(result); + } + + logger.info(`Successfully deleted object view ${id}`); + res.send({ + status: 'ok', + }); +}; diff --git a/src/utils.js b/src/utils.js index d054547..c8b6c69 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1186,7 +1186,7 @@ function getChangedValues(oldObj, newObj, old = false) { return changes; } -const AUDIT_EXCLUDED_MODELS = ['notification', 'userNotifier', 'marketplaceEvent']; +const AUDIT_EXCLUDED_MODELS = ['notification', 'userNotifier', 'objectView', 'marketplaceEvent']; const SENSITIVE_KEYS = ['secret']; const DISTRIBUTE_KEYS = {