Enhance database functionality by adding user parameter to edit, new, and delete object functions; implement email account, template, and message schemas; update audit log schema to include system flag and improve owner resolution logic.
All checks were successful
farmcontrol/farmcontrol-scheduler/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-scheduler/pipeline/head This commit looks good
This commit is contained in:
parent
10b5074f94
commit
39060f0cea
@ -512,6 +512,7 @@ export const editObject = async ({
|
|||||||
model,
|
model,
|
||||||
id,
|
id,
|
||||||
updateData,
|
updateData,
|
||||||
|
user = undefined,
|
||||||
owner = undefined,
|
owner = undefined,
|
||||||
ownerType = undefined,
|
ownerType = undefined,
|
||||||
populate = [],
|
populate = [],
|
||||||
@ -546,7 +547,10 @@ export const editObject = async ({
|
|||||||
const previousExpandedObject = expandObjectIds(previousObject);
|
const previousExpandedObject = expandObjectIds(previousObject);
|
||||||
const newExpandedObject = expandObjectIds(newObject);
|
const newExpandedObject = expandObjectIds(newObject);
|
||||||
|
|
||||||
if (auditLog == true && owner != undefined && ownerType != undefined) {
|
if (
|
||||||
|
auditLog == true &&
|
||||||
|
(user != undefined || (owner != undefined && ownerType != undefined))
|
||||||
|
) {
|
||||||
// Audit log before update
|
// Audit log before update
|
||||||
await editAuditLog(
|
await editAuditLog(
|
||||||
previousExpandedObject,
|
previousExpandedObject,
|
||||||
@ -555,6 +559,7 @@ export const editObject = async ({
|
|||||||
parentType,
|
parentType,
|
||||||
owner,
|
owner,
|
||||||
ownerType,
|
ownerType,
|
||||||
|
user,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -589,7 +594,7 @@ export const editObject = async ({
|
|||||||
|
|
||||||
if (model.recalculate && recalculate == true) {
|
if (model.recalculate && recalculate == true) {
|
||||||
logger.debug(`Recalculating ${model.modelName}`);
|
logger.debug(`Recalculating ${model.modelName}`);
|
||||||
await model.recalculate(newExpandedObject, owner, ownerType);
|
await model.recalculate(newExpandedObject, user ?? owner, ownerType);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.stats) {
|
if (model.stats) {
|
||||||
@ -609,6 +614,7 @@ export const editObject = async ({
|
|||||||
export const newObject = async ({
|
export const newObject = async ({
|
||||||
model,
|
model,
|
||||||
newData,
|
newData,
|
||||||
|
user = undefined,
|
||||||
owner = null,
|
owner = null,
|
||||||
ownerType = undefined,
|
ownerType = undefined,
|
||||||
}) => {
|
}) => {
|
||||||
@ -621,8 +627,15 @@ export const newObject = async ({
|
|||||||
}
|
}
|
||||||
const created = result;
|
const created = result;
|
||||||
|
|
||||||
if (owner != undefined && ownerType != undefined) {
|
if (user != undefined || (owner != undefined && ownerType != undefined)) {
|
||||||
await newAuditLog(newData, created._id, parentType, owner, ownerType);
|
await newAuditLog(
|
||||||
|
newData,
|
||||||
|
created._id,
|
||||||
|
parentType,
|
||||||
|
owner,
|
||||||
|
ownerType,
|
||||||
|
user,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await distributeNew(created, parentType);
|
await distributeNew(created, parentType);
|
||||||
@ -645,6 +658,7 @@ export const newObject = async ({
|
|||||||
export const deleteObject = async ({
|
export const deleteObject = async ({
|
||||||
model,
|
model,
|
||||||
id,
|
id,
|
||||||
|
user = undefined,
|
||||||
owner = null,
|
owner = null,
|
||||||
ownerType = undefined,
|
ownerType = undefined,
|
||||||
}) => {
|
}) => {
|
||||||
@ -657,9 +671,9 @@ export const deleteObject = async ({
|
|||||||
return { error: `${parentType} not found.`, code: 404 };
|
return { error: `${parentType} not found.`, code: 404 };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (owner != undefined && ownerType != undefined) {
|
if (user != undefined || (owner != undefined && ownerType != undefined)) {
|
||||||
// Audit log the deletion
|
// 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() });
|
deleteObjectCache({ model: model, id: id.toString() });
|
||||||
|
|||||||
@ -7,36 +7,45 @@ const auditLogSchema = new Schema(
|
|||||||
_reference: { type: String, default: () => generateId()() },
|
_reference: { type: String, default: () => generateId()() },
|
||||||
changes: {
|
changes: {
|
||||||
old: { type: Object, required: false },
|
old: { type: Object, required: false },
|
||||||
new: { type: Object, required: false },
|
new: { type: Object, required: false }
|
||||||
},
|
},
|
||||||
operation: {
|
operation: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true
|
||||||
},
|
},
|
||||||
parent: {
|
parent: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
refPath: 'parentType',
|
refPath: 'parentType',
|
||||||
required: true,
|
required: true
|
||||||
},
|
},
|
||||||
parentType: {
|
parentType: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true
|
||||||
},
|
},
|
||||||
owner: {
|
owner: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
refPath: 'ownerType',
|
refPath: 'ownerType',
|
||||||
required: true,
|
required: false
|
||||||
},
|
},
|
||||||
ownerType: {
|
ownerType: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: false,
|
||||||
enum: ['user', 'printer', 'host', 'marketplace'],
|
enum: ['user', 'printer', 'host', 'marketplace']
|
||||||
},
|
},
|
||||||
|
system: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
auditLogSchema.index({ operation: 'text', parentType: 'text', ownerType: 'text' });
|
auditLogSchema.index({
|
||||||
|
operation: 'text',
|
||||||
|
parentType: 'text',
|
||||||
|
ownerType: 'text'
|
||||||
|
});
|
||||||
|
|
||||||
// Add virtual id getter
|
// Add virtual id getter
|
||||||
auditLogSchema.virtual('id').get(function () {
|
auditLogSchema.virtual('id').get(function () {
|
||||||
|
|||||||
33
src/database/schemas/management/emailaccount.schema.js
Normal file
33
src/database/schemas/management/emailaccount.schema.js
Normal file
@ -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);
|
||||||
68
src/database/schemas/management/emailmessage.schema.js
Normal file
68
src/database/schemas/management/emailmessage.schema.js
Normal file
@ -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
|
||||||
|
);
|
||||||
78
src/database/schemas/management/emailtemplate.schema.js
Normal file
78
src/database/schemas/management/emailtemplate.schema.js
Normal file
@ -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: '<Container></Container>' },
|
||||||
|
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);
|
||||||
@ -35,6 +35,9 @@ import { userNotifierModel } from './misc/usernotifier.schema.js';
|
|||||||
import { objectViewModel } from './misc/objectview.schema.js';
|
import { objectViewModel } from './misc/objectview.schema.js';
|
||||||
import { documentSizeModel } from './management/documentsize.schema.js';
|
import { documentSizeModel } from './management/documentsize.schema.js';
|
||||||
import { documentTemplateModel } from './management/documenttemplate.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 { hostModel } from './management/host.schema.js';
|
||||||
import { documentPrinterModel } from './management/documentprinter.schema.js';
|
import { documentPrinterModel } from './management/documentprinter.schema.js';
|
||||||
import { documentJobModel } from './management/documentjob.schema.js';
|
import { documentJobModel } from './management/documentjob.schema.js';
|
||||||
@ -107,6 +110,9 @@ export const models = {
|
|||||||
OVW: modelEntry(() => objectViewModel, 'objectView', 'Object View'),
|
OVW: modelEntry(() => objectViewModel, 'objectView', 'Object View'),
|
||||||
DSZ: modelEntry(() => documentSizeModel, 'documentSize', 'Document Size'),
|
DSZ: modelEntry(() => documentSizeModel, 'documentSize', 'Document Size'),
|
||||||
DTP: modelEntry(() => documentTemplateModel, 'documentTemplate', 'Document Template'),
|
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'),
|
DPR: modelEntry(() => documentPrinterModel, 'documentPrinter', 'Document Printer'),
|
||||||
DJB: modelEntry(() => documentJobModel, 'documentJob', 'Document Job'),
|
DJB: modelEntry(() => documentJobModel, 'documentJob', 'Document Job'),
|
||||||
HST: modelEntry(() => hostModel, 'host', 'Host'),
|
HST: modelEntry(() => hostModel, 'host', 'Host'),
|
||||||
|
|||||||
@ -19,7 +19,7 @@ const AUDIT_EXCLUDED_MODELS = [
|
|||||||
'marketplaceEvent'
|
'marketplaceEvent'
|
||||||
];
|
];
|
||||||
const AUDIT_EXCLUDED_CHANGES = ['state.message'];
|
const AUDIT_EXCLUDED_CHANGES = ['state.message'];
|
||||||
const SENSITIVE_KEYS = ['secret'];
|
const SENSITIVE_KEYS = ['secret', 'password'];
|
||||||
|
|
||||||
function omitSensitive(obj) {
|
function omitSensitive(obj) {
|
||||||
if (obj == null || typeof obj !== 'object') return obj;
|
if (obj == null || typeof obj !== 'object') return obj;
|
||||||
@ -32,6 +32,25 @@ function omitSensitive(obj) {
|
|||||||
return result;
|
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) {
|
function omitPath(obj, path) {
|
||||||
if (obj == null || typeof obj !== 'object') return obj;
|
if (obj == null || typeof obj !== 'object') return obj;
|
||||||
const keys = path.split('.');
|
const keys = path.split('.');
|
||||||
@ -450,7 +469,14 @@ function getChangedValues(oldObj, newObj, old = false) {
|
|||||||
return changes;
|
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;
|
if (AUDIT_EXCLUDED_MODELS.includes(parentType)) return;
|
||||||
|
|
||||||
// Filter out createdAt, updatedAt, and sensitive fields from newValue
|
// Filter out createdAt, updatedAt, and sensitive fields from newValue
|
||||||
@ -463,8 +489,7 @@ async function newAuditLog(newValue, parentId, parentType, owner, ownerType) {
|
|||||||
},
|
},
|
||||||
parent: parentId,
|
parent: parentId,
|
||||||
parentType,
|
parentType,
|
||||||
owner: owner._id,
|
...resolveAuditOwner(owner, ownerType, user),
|
||||||
ownerType: ownerType,
|
|
||||||
operation: 'new'
|
operation: 'new'
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -479,7 +504,8 @@ async function editAuditLog(
|
|||||||
parentId,
|
parentId,
|
||||||
parentType,
|
parentType,
|
||||||
owner,
|
owner,
|
||||||
ownerType
|
ownerType,
|
||||||
|
user
|
||||||
) {
|
) {
|
||||||
if (parentType === 'stockEvent') {
|
if (parentType === 'stockEvent') {
|
||||||
return;
|
return;
|
||||||
@ -513,8 +539,7 @@ async function editAuditLog(
|
|||||||
},
|
},
|
||||||
parent: parentId,
|
parent: parentId,
|
||||||
parentType,
|
parentType,
|
||||||
owner: owner._id,
|
...resolveAuditOwner(owner, ownerType, user),
|
||||||
ownerType: ownerType,
|
|
||||||
operation: 'edit'
|
operation: 'edit'
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -528,7 +553,8 @@ async function deleteAuditLog(
|
|||||||
parentId,
|
parentId,
|
||||||
parentType,
|
parentType,
|
||||||
owner,
|
owner,
|
||||||
ownerType
|
ownerType,
|
||||||
|
user
|
||||||
) {
|
) {
|
||||||
const auditLog = new auditLogModel({
|
const auditLog = new auditLogModel({
|
||||||
changes: {
|
changes: {
|
||||||
@ -536,8 +562,7 @@ async function deleteAuditLog(
|
|||||||
},
|
},
|
||||||
parent: parentId,
|
parent: parentId,
|
||||||
parentType,
|
parentType,
|
||||||
owner: owner._id,
|
...resolveAuditOwner(owner, ownerType, user),
|
||||||
ownerType: ownerType,
|
|
||||||
operation: 'delete'
|
operation: 'delete'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user