diff --git a/src/database/database.js b/src/database/database.js index 2d8df55..45b86b4 100644 --- a/src/database/database.js +++ b/src/database/database.js @@ -22,6 +22,16 @@ const cacheLogger = log4js.getLogger('Local Cache'); logger.level = config.server.logLevel; cacheLogger.level = config.server.logLevel; +const mergeObjectUpdates = (target, source) => + _.mergeWith(target, source, (objValue, srcValue, key) => { + if (Array.isArray(objValue) || Array.isArray(srcValue)) { + return srcValue; + } + if (key === 'permissions' && srcValue !== undefined) { + return srcValue; + } + }); + // Default cache TTL in seconds (similar to previous in-memory cache) const CACHE_TTL_SECONDS = config.database?.redis?.ttlSeconds || 5; @@ -69,13 +79,13 @@ export const updateObjectCache = async ({ for (const key of matchingKeys) { logger.trace('Updating object cache:', key); const cachedObject = (await redisServer.getKey(key)) || {}; - const mergedObject = _.merge(cachedObject, object); + const mergedObject = mergeObjectUpdates(cachedObject, object); await redisServer.setKey(key, mergedObject, CACHE_TTL_SECONDS); mergedObjects.push(mergedObject); } const cacheObject = (await redisServer.getKey(cacheKey)) || {}; - const mergedObject = _.merge(cacheObject, object); + const mergedObject = mergeObjectUpdates(cacheObject, object); await redisServer.setKey(cacheKey, mergedObject, CACHE_TTL_SECONDS); cacheLogger.trace('Updated:', { diff --git a/src/database/permissions.js b/src/database/permissions.js index 73d5b4a..45f30e1 100644 --- a/src/database/permissions.js +++ b/src/database/permissions.js @@ -1,3 +1,5 @@ +import mongoose from 'mongoose'; + export const applyPermissionSettingsList = (settingsList = []) => { const permissions = {}; @@ -36,3 +38,25 @@ export const getPermissionSettingsId = (value) => { } return value; }; + +export const excludeIdFromList = (items = [], id) => + (items || []).filter((item) => String(getPermissionSettingsId(item)) !== String(id)); + +export const resolveReferencedDocs = async (modelName, items = []) => { + const ids = (items || []).map(getPermissionSettingsId).filter(Boolean); + if (ids.length === 0) { + return []; + } + + const docs = await mongoose + .model(modelName) + .find({ _id: { $in: ids } }) + .lean(); + const docsById = new Map(docs.map((doc) => [String(doc._id), doc])); + + return ids.map((id) => docsById.get(String(id))).filter(Boolean); +}; + +export const resolvePermissionSettings = async (owner) => { + return resolveReferencedDocs('permissionSetting', owner?.permissionSettings); +}; diff --git a/src/database/schemas/management/permissionsetting.schema.js b/src/database/schemas/management/permissionsetting.schema.js new file mode 100644 index 0000000..bc9aefa --- /dev/null +++ b/src/database/schemas/management/permissionsetting.schema.js @@ -0,0 +1,62 @@ +import mongoose from 'mongoose'; +import { generateId } from '../../utils.js'; +import { excludeIdFromList, getPermissionSettingsId } from '../../permissions.js'; + +const { Schema } = mongoose; + +const permissionSettingSchema = new Schema( + { + _reference: { type: String, default: () => generateId()() }, + name: { type: String, required: true }, + permissions: { type: Schema.Types.Mixed, required: false, default: () => ({}) }, + }, + { timestamps: true } +); + +permissionSettingSchema.index({ name: 'text' }); + +permissionSettingSchema.virtual('id').get(function () { + return this._id; +}); + +permissionSettingSchema.set('toJSON', { virtuals: true }); + +const recalculateAssignees = async (model, permissionSettingId, stillExists, user) => { + const assigned = await model.find({ permissionSettings: permissionSettingId }).lean(); + + if (!stillExists) { + await model.updateMany( + { permissionSettings: permissionSettingId }, + { $pull: { permissionSettings: permissionSettingId } } + ); + } + + for (const item of assigned) { + const nextItem = stillExists + ? item + : { + ...item, + permissionSettings: excludeIdFromList(item.permissionSettings, permissionSettingId), + }; + await model.recalculate(nextItem, user); + } +}; + +permissionSettingSchema.statics.recalculate = async function (permissionSetting, user) { + const permissionSettingId = getPermissionSettingsId(permissionSetting); + if (!permissionSettingId) { + return; + } + + const stillExists = await this.exists({ _id: permissionSettingId }); + const userGroupModel = mongoose.model('userGroup'); + const userModel = mongoose.model('user'); + + await recalculateAssignees(userGroupModel, permissionSettingId, stillExists, user); + await recalculateAssignees(userModel, permissionSettingId, stillExists, user); +}; + +export const permissionSettingModel = mongoose.model( + 'permissionSetting', + permissionSettingSchema +); diff --git a/src/database/schemas/management/permissionsettings.schema.js b/src/database/schemas/management/permissionsettings.schema.js deleted file mode 100644 index 688c38f..0000000 --- a/src/database/schemas/management/permissionsettings.schema.js +++ /dev/null @@ -1,59 +0,0 @@ -import mongoose from 'mongoose'; -import { generateId } from '../../utils.js'; -import { getPermissionSettingsId } from '../../permissions.js'; - -const { Schema } = mongoose; - -const permissionSettingsSchema = new Schema( - { - _reference: { type: String, default: () => generateId()() }, - name: { type: String, required: true }, - permissions: { type: Schema.Types.Mixed, required: false, default: () => ({}) }, - }, - { timestamps: true } -); - -permissionSettingsSchema.index({ name: 'text' }); - -permissionSettingsSchema.virtual('id').get(function () { - return this._id; -}); - -permissionSettingsSchema.set('toJSON', { virtuals: true }); - -permissionSettingsSchema.statics.recalculate = async function (permissionSettings, user) { - const permissionSettingsId = getPermissionSettingsId(permissionSettings); - if (!permissionSettingsId) { - return; - } - - const userModel = mongoose.model('user'); - const users = await userModel - .find({ permissionSettings: permissionSettingsId }) - .lean(); - - const stillExists = await this.exists({ _id: permissionSettingsId }); - if (!stillExists) { - await userModel.updateMany( - { permissionSettings: permissionSettingsId }, - { $pull: { permissionSettings: permissionSettingsId } } - ); - } - - for (const assignedUser of users) { - const nextUser = stillExists - ? assignedUser - : { - ...assignedUser, - permissionSettings: (assignedUser.permissionSettings || []).filter( - (item) => String(getPermissionSettingsId(item)) !== String(permissionSettingsId) - ), - }; - await userModel.recalculate(nextUser, user); - } -}; - -export const permissionSettingsModel = mongoose.model( - 'permissionSettings', - permissionSettingsSchema -); diff --git a/src/database/schemas/management/user.schema.js b/src/database/schemas/management/user.schema.js index 1da0e62..0112da3 100644 --- a/src/database/schemas/management/user.schema.js +++ b/src/database/schemas/management/user.schema.js @@ -1,6 +1,10 @@ import mongoose from 'mongoose'; import { generateId } from '../../utils.js'; -import { applyPermissionSettingsList, getPermissionSettingsId } from '../../permissions.js'; +import { + applyPermissionSettingsList, + resolvePermissionSettings, + resolveReferencedDocs, +} from '../../permissions.js'; const { Schema } = mongoose; @@ -14,8 +18,9 @@ const userSchema = new mongoose.Schema( email: { required: true, type: String }, profileImage: { type: mongoose.SchemaTypes.ObjectId, ref: 'file', required: false }, appPasswordHash: { type: String, required: false, select: false }, + groups: [{ type: Schema.Types.ObjectId, ref: 'userGroup', required: false }], permissionSettings: [ - { type: Schema.Types.ObjectId, ref: 'permissionSettings', required: false }, + { type: Schema.Types.ObjectId, ref: 'permissionSetting', required: false }, ], permissions: { type: Schema.Types.Mixed, required: false, default: () => ({}) }, }, @@ -30,30 +35,18 @@ userSchema.virtual('id').get(function () { userSchema.set('toJSON', { virtuals: true }); -const resolvePermissionSettings = async (user) => { - const permissionSettingsModel = mongoose.model('permissionSettings'); - const items = user?.permissionSettings || []; - const ids = items.map(getPermissionSettingsId).filter(Boolean); - if (ids.length === 0) { - return []; - } - - const docs = await permissionSettingsModel - .find({ _id: { $in: ids } }) - .lean(); - const docsById = new Map(docs.map((doc) => [String(doc._id), doc])); - - return ids.map((id) => docsById.get(String(id))).filter(Boolean); -}; - userSchema.statics.recalculate = async function (user, actingUser) { const userId = user?._id || user; if (!userId) { return; } + const groups = await resolveReferencedDocs('userGroup', user?.groups); const settings = await resolvePermissionSettings(user); - const permissions = applyPermissionSettingsList(settings); + const permissions = applyPermissionSettingsList([...groups, ...settings]); + if (user && typeof user === 'object' && !user._bsontype) { + user.permissions = permissions; + } const { editObject } = await import('../../database.js'); await editObject({ @@ -61,7 +54,7 @@ userSchema.statics.recalculate = async function (user, actingUser) { id: userId, updateData: { permissions }, user: actingUser, - populate: ['profileImage', 'permissionSettings'], + populate: ['profileImage', 'permissionSettings', 'groups'], recalculate: false, }); }; diff --git a/src/database/schemas/management/usergroup.schema.js b/src/database/schemas/management/usergroup.schema.js new file mode 100644 index 0000000..a748e59 --- /dev/null +++ b/src/database/schemas/management/usergroup.schema.js @@ -0,0 +1,74 @@ +import mongoose from 'mongoose'; +import { generateId } from '../../utils.js'; +import { + applyPermissionSettingsList, + excludeIdFromList, + resolvePermissionSettings, +} from '../../permissions.js'; + +const { Schema } = mongoose; + +const userGroupSchema = new Schema( + { + _reference: { type: String, default: () => generateId()() }, + name: { type: String, required: true }, + permissionSettings: [ + { type: Schema.Types.ObjectId, ref: 'permissionSetting', required: false }, + ], + permissions: { type: Schema.Types.Mixed, required: false, default: () => ({}) }, + }, + { timestamps: true } +); + +userGroupSchema.index({ name: 'text' }); + +userGroupSchema.virtual('id').get(function () { + return this._id; +}); + +userGroupSchema.set('toJSON', { virtuals: true }); + +userGroupSchema.statics.recalculate = async function (userGroup, actingUser) { + const userGroupId = userGroup?._id || userGroup; + if (!userGroupId) { + return; + } + + const stillExists = await this.exists({ _id: userGroupId }); + if (stillExists) { + const settings = await resolvePermissionSettings(userGroup); + const permissions = applyPermissionSettingsList(settings); + if (userGroup && typeof userGroup === 'object' && !userGroup._bsontype) { + userGroup.permissions = permissions; + } + const { editObject } = await import('../../database.js'); + + await editObject({ + model: this, + id: userGroupId, + updateData: { permissions }, + user: actingUser, + populate: ['permissionSettings'], + recalculate: false, + }); + } + + const userModel = mongoose.model('user'); + const users = await userModel.find({ groups: userGroupId }).lean(); + + if (!stillExists) { + await userModel.updateMany({ groups: userGroupId }, { $pull: { groups: userGroupId } }); + } + + for (const assignedUser of users) { + const nextUser = stillExists + ? assignedUser + : { + ...assignedUser, + groups: excludeIdFromList(assignedUser.groups, userGroupId), + }; + await userModel.recalculate(nextUser, actingUser); + } +}; + +export const userGroupModel = mongoose.model('userGroup', userGroupSchema); diff --git a/src/database/schemas/models.js b/src/database/schemas/models.js index 7a63124..e4276e6 100644 --- a/src/database/schemas/models.js +++ b/src/database/schemas/models.js @@ -24,7 +24,8 @@ import { stockLocationModel } from './inventory/stocklocation.schema.js'; import { stockTransferModel } from './inventory/stocktransfer.schema.js'; import { auditLogModel } from './management/auditlog.schema.js'; import { userModel } from './management/user.schema.js'; -import { permissionSettingsModel } from './management/permissionsettings.schema.js'; +import { userGroupModel } from './management/usergroup.schema.js'; +import { permissionSettingModel } from './management/permissionsetting.schema.js'; import { appPasswordModel } from './management/apppassword.schema.js'; import { noteTypeModel } from './management/notetype.schema.js'; import { noteModel } from './misc/note.schema.js'; @@ -213,10 +214,17 @@ export const models = { referenceField: '_reference', label: 'User', }, - PMS: { - model: permissionSettingsModel, + UGP: { + model: userGroupModel, idField: '_id', - type: 'permissionSettings', + type: 'userGroup', + referenceField: '_reference', + label: 'User Group', + }, + PMS: { + model: permissionSettingModel, + idField: '_id', + type: 'permissionSetting', referenceField: '_reference', label: 'Permission Settings', },