diff --git a/src/database/database.js b/src/database/database.js index 995d8eb..07d5548 100644 --- a/src/database/database.js +++ b/src/database/database.js @@ -512,6 +512,7 @@ export const editObject = async ({ model, id, updateData, + user = undefined, owner = undefined, ownerType = undefined, populate = [], @@ -546,7 +547,10 @@ export const editObject = async ({ const previousExpandedObject = expandObjectIds(previousObject); const newExpandedObject = expandObjectIds(newObject); - if (auditLog == true && owner != undefined && ownerType != undefined) { + if ( + auditLog == true && + (user != undefined || (owner != undefined && ownerType != undefined)) + ) { // Audit log before update await editAuditLog( previousExpandedObject, @@ -554,7 +558,8 @@ export const editObject = async ({ id, parentType, owner, - ownerType + ownerType, + user ); } @@ -589,7 +594,7 @@ export const editObject = async ({ if (model.recalculate && recalculate == true) { logger.debug(`Recalculating ${model.modelName}`); - await model.recalculate(newExpandedObject, owner, ownerType); + await model.recalculate(newExpandedObject, user ?? owner, ownerType); } if (model.stats) { @@ -609,6 +614,7 @@ export const editObject = async ({ export const newObject = async ({ model, newData, + user = undefined, owner = null, ownerType = undefined }) => { @@ -621,8 +627,15 @@ export const newObject = async ({ } const created = result; - if (owner != undefined && ownerType != undefined) { - await newAuditLog(newData, created._id, parentType, owner, ownerType); + if (user != undefined || (owner != undefined && ownerType != undefined)) { + await newAuditLog( + newData, + created._id, + parentType, + owner, + ownerType, + user + ); } await distributeNew(created, parentType); @@ -645,6 +658,7 @@ export const newObject = async ({ export const deleteObject = async ({ model, id, + user = undefined, owner = null, ownerType = undefined }) => { @@ -657,9 +671,9 @@ export const deleteObject = async ({ return { error: `${parentType} not found.`, code: 404 }; } - if (owner != undefined && ownerType != undefined) { + if (user != undefined || (owner != undefined && ownerType != undefined)) { // Audit log the deletion - await deleteAuditLog(result, id, parentType, owner, ownerType); + await deleteAuditLog(result, id, parentType, owner, ownerType, user); } deleteObjectCache({ model: model, id: id.toString() }); diff --git a/src/database/schemas/management/auditlog.schema.js b/src/database/schemas/management/auditlog.schema.js index 1ac2bcf..0c07d1c 100644 --- a/src/database/schemas/management/auditlog.schema.js +++ b/src/database/schemas/management/auditlog.schema.js @@ -7,36 +7,45 @@ const auditLogSchema = new Schema( _reference: { type: String, default: () => generateId()() }, changes: { old: { type: Object, required: false }, - new: { type: Object, required: false }, + new: { type: Object, required: false } }, operation: { type: String, - required: true, + required: true }, parent: { type: Schema.Types.ObjectId, refPath: 'parentType', - required: true, + required: true }, parentType: { type: String, - required: true, + required: true }, owner: { type: Schema.Types.ObjectId, refPath: 'ownerType', - required: true, + required: false }, ownerType: { type: String, - required: true, - enum: ['user', 'printer', 'host', 'marketplace'], + required: false, + enum: ['user', 'printer', 'host', 'marketplace'] }, + system: { + type: Boolean, + required: true, + default: false + } }, { timestamps: true } ); -auditLogSchema.index({ operation: 'text', parentType: 'text', ownerType: 'text' }); +auditLogSchema.index({ + operation: 'text', + parentType: 'text', + ownerType: 'text' +}); // Add virtual id getter auditLogSchema.virtual('id').get(function () { diff --git a/src/database/schemas/management/emailaccount.schema.js b/src/database/schemas/management/emailaccount.schema.js new file mode 100644 index 0000000..5ff8335 --- /dev/null +++ b/src/database/schemas/management/emailaccount.schema.js @@ -0,0 +1,33 @@ +import mongoose from 'mongoose'; +import { generateId } from '../../utils.js'; + +const { Schema } = mongoose; + +const emailAccountSchema = new Schema( + { + _reference: { type: String, default: () => generateId()() }, + name: { type: String, required: true, unique: true }, + active: { type: Boolean, required: true, default: true }, + host: { type: String, required: true }, + port: { type: Number, required: true, default: 587, min: 1, max: 65535 }, + secure: { type: Boolean, required: true, default: false }, + username: { type: String, required: false }, + password: { type: String, required: false, select: false }, + fromName: { type: String, required: true }, + fromEmail: { type: String, required: true }, + }, + { timestamps: true } +); + +emailAccountSchema.index({ name: 'text', host: 'text', fromEmail: 'text' }); +emailAccountSchema.virtual('id').get(function () { + return this._id; +}); +function omitPassword(_document, value) { + delete value.password; + return value; +} +emailAccountSchema.set('toJSON', { virtuals: true, transform: omitPassword }); +emailAccountSchema.set('toObject', { virtuals: true, transform: omitPassword }); + +export const emailAccountModel = mongoose.model('emailAccount', emailAccountSchema); diff --git a/src/database/schemas/management/emailmessage.schema.js b/src/database/schemas/management/emailmessage.schema.js new file mode 100644 index 0000000..4152907 --- /dev/null +++ b/src/database/schemas/management/emailmessage.schema.js @@ -0,0 +1,68 @@ +import mongoose from 'mongoose'; +import { generateId } from '../../utils.js'; + +const { Schema } = mongoose; + +const emailMessageSchema = new Schema( + { + _reference: { type: String, default: () => generateId()() }, + name: { type: String, required: true }, + emailTemplate: { + type: Schema.Types.ObjectId, + ref: 'emailTemplate', + required: true + }, + objectType: { type: String, required: true }, + object: { + type: Schema.Types.ObjectId, + refPath: 'objectType', + required: true + }, + emailAccount: { + type: Schema.Types.ObjectId, + ref: 'emailAccount', + required: true + }, + recipientEmail: { type: String, required: true }, + recipientType: { + type: String, + enum: ['client', 'vendor'], + required: false + }, + recipient: { + type: Schema.Types.ObjectId, + refPath: 'recipientType', + required: false + }, + fromEmail: { type: String, required: true, immutable: true }, + messageId: { type: String, required: false }, + subject: { type: String, required: false, default: '' }, + content: { type: String, required: false, default: '' }, + attachments: [{ type: Schema.Types.ObjectId, ref: 'file', required: false }], + state: { + type: { type: String, required: true, default: 'queued' }, + progress: { type: Number, required: false, default: 0 }, + message: { type: String, required: false } + }, + read: { type: Boolean, required: true, default: false }, + sentAt: { type: Date, required: false }, + readAt: { type: Date, required: false } + }, + { timestamps: true, suppressReservedKeysWarning: true } +); + +emailMessageSchema.index({ + name: 'text', + recipientEmail: 'text', + fromEmail: 'text', + objectType: 'text' +}); +emailMessageSchema.virtual('id').get(function () { + return this._id; +}); +emailMessageSchema.set('toJSON', { virtuals: true }); + +export const emailMessageModel = mongoose.model( + 'emailMessage', + emailMessageSchema +); diff --git a/src/database/schemas/management/emailtemplate.schema.js b/src/database/schemas/management/emailtemplate.schema.js new file mode 100644 index 0000000..612fe8f --- /dev/null +++ b/src/database/schemas/management/emailtemplate.schema.js @@ -0,0 +1,78 @@ +import mongoose from 'mongoose'; +import { generateId } from '../../utils.js'; + +const { Schema } = mongoose; +const RENDER_EMAIL_TEMPLATE_CALL = /fc\.renderEmailTemplate\s*\(\s*(['"])([^'"]+)\1/g; + +function extractReferences(content) { + const references = []; + const seen = new Set(); + for (const match of String(content || '').matchAll(RENDER_EMAIL_TEMPLATE_CALL)) { + const reference = match[2]?.trim(); + if (reference && !seen.has(reference)) { + seen.add(reference); + references.push(reference); + } + } + return references; +} + +const emailTemplateAttachmentSchema = new Schema( + { + type: { type: String, enum: ['file', 'documentTemplate'], required: true }, + file: { type: Schema.Types.ObjectId, refPath: 'attachments.type', required: true }, + fileName: { type: String, required: true }, + fileType: { type: String, enum: ['pdf', 'jpeg', 'png', 'svg'], required: false }, + }, + { _id: true } +); + +const emailTemplateSchema = new Schema( + { + _reference: { type: String, default: () => generateId()() }, + name: { type: String, required: true, unique: true }, + objectType: { type: String, required: false }, + tags: [{ type: String }], + active: { type: Boolean, required: true, default: true }, + global: { type: Boolean, required: true, default: false }, + parent: { type: Schema.Types.ObjectId, ref: 'emailTemplate', required: false }, + referencedTemplates: [ + { type: Schema.Types.ObjectId, ref: 'emailTemplate', required: false }, + ], + subject: { type: String, required: false, default: '' }, + content: { type: String, required: false, default: '' }, + testObject: { type: Schema.Types.ObjectId, refPath: 'objectType', required: false }, + attachments: { type: [emailTemplateAttachmentSchema], default: [] }, + }, + { timestamps: true } +); + +emailTemplateSchema.index({ name: 'text', tags: 'text', objectType: 'text' }); +emailTemplateSchema.virtual('id').get(function () { + return this._id; +}); +emailTemplateSchema.set('toJSON', { virtuals: true }); + +emailTemplateSchema.statics.recalculate = async function (emailTemplate, user) { + const id = emailTemplate?._id || emailTemplate; + if (!id || !(await this.exists({ _id: id }))) return; + + const referencedTemplates = []; + for (const reference of extractReferences(emailTemplate?.content)) { + const normalizedReference = reference.replace(/^ETP:/i, '').trim(); + const found = await this.findOne({ _reference: normalizedReference }).select('_id').lean(); + if (found?._id) referencedTemplates.push(found._id); + } + + const { editObject } = await import('../../database.js'); + await editObject({ + model: this, + id, + updateData: { referencedTemplates }, + user, + populate: [{ path: 'referencedTemplates', strictPopulate: false }], + recalculate: false, + }); +}; + +export const emailTemplateModel = mongoose.model('emailTemplate', emailTemplateSchema); diff --git a/src/database/schemas/models.js b/src/database/schemas/models.js index 9c67b04..fa4395b 100644 --- a/src/database/schemas/models.js +++ b/src/database/schemas/models.js @@ -35,6 +35,9 @@ 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 { emailAccountModel } from './management/emailaccount.schema.js'; +import { emailTemplateModel } from './management/emailtemplate.schema.js'; +import { emailMessageModel } from './management/emailmessage.schema.js'; import { hostModel } from './management/host.schema.js'; import { documentPrinterModel } from './management/documentprinter.schema.js'; import { documentJobModel } from './management/documentjob.schema.js'; @@ -107,6 +110,9 @@ export const models = { OVW: modelEntry(() => objectViewModel, 'objectView', 'Object View'), DSZ: modelEntry(() => documentSizeModel, 'documentSize', 'Document Size'), DTP: modelEntry(() => documentTemplateModel, 'documentTemplate', 'Document Template'), + EAC: modelEntry(() => emailAccountModel, 'emailAccount', 'Email Account'), + ETP: modelEntry(() => emailTemplateModel, 'emailTemplate', 'Email Template'), + EMS: modelEntry(() => emailMessageModel, 'emailMessage', 'Email Message'), DPR: modelEntry(() => documentPrinterModel, 'documentPrinter', 'Document Printer'), DJB: modelEntry(() => documentJobModel, 'documentJob', 'Document Job'), HST: modelEntry(() => hostModel, 'host', 'Host'), diff --git a/src/database/utils.js b/src/database/utils.js index a5ee793..0026f95 100644 --- a/src/database/utils.js +++ b/src/database/utils.js @@ -19,7 +19,7 @@ const AUDIT_EXCLUDED_MODELS = [ 'marketplaceEvent' ]; const AUDIT_EXCLUDED_CHANGES = ['state.message']; -const SENSITIVE_KEYS = ['secret']; +const SENSITIVE_KEYS = ['secret', 'password']; function omitSensitive(obj) { if (obj == null || typeof obj !== 'object') return obj; @@ -32,6 +32,25 @@ function omitSensitive(obj) { return result; } +function resolveAuditOwner(owner, ownerType, user) { + if (user === 'system') return { system: true }; + + const actor = user ?? owner; + const resolvedOwner = actor?._id ?? actor; + const resolvedOwnerType = + ownerType ?? + (['user', 'printer', 'host', 'marketplace'].includes(actor?._objectType) + ? actor._objectType + : 'user'); + + return { + system: false, + ...(resolvedOwner != null + ? { owner: resolvedOwner, ownerType: resolvedOwnerType } + : {}) + }; +} + function omitPath(obj, path) { if (obj == null || typeof obj !== 'object') return obj; const keys = path.split('.'); @@ -450,7 +469,14 @@ function getChangedValues(oldObj, newObj, old = false) { return changes; } -async function newAuditLog(newValue, parentId, parentType, owner, ownerType) { +async function newAuditLog( + newValue, + parentId, + parentType, + owner, + ownerType, + user +) { if (AUDIT_EXCLUDED_MODELS.includes(parentType)) return; // Filter out createdAt, updatedAt, and sensitive fields from newValue @@ -463,8 +489,7 @@ async function newAuditLog(newValue, parentId, parentType, owner, ownerType) { }, parent: parentId, parentType, - owner: owner._id, - ownerType: ownerType, + ...resolveAuditOwner(owner, ownerType, user), operation: 'new' }); @@ -479,7 +504,8 @@ async function editAuditLog( parentId, parentType, owner, - ownerType + ownerType, + user ) { if (parentType === 'stockEvent') { return; @@ -513,8 +539,7 @@ async function editAuditLog( }, parent: parentId, parentType, - owner: owner._id, - ownerType: ownerType, + ...resolveAuditOwner(owner, ownerType, user), operation: 'edit' }); @@ -528,7 +553,8 @@ async function deleteAuditLog( parentId, parentType, owner, - ownerType + ownerType, + user ) { const auditLog = new auditLogModel({ changes: { @@ -536,8 +562,7 @@ async function deleteAuditLog( }, parent: parentId, parentType, - owner: owner._id, - ownerType: ownerType, + ...resolveAuditOwner(owner, ownerType, user), operation: 'delete' });