diff --git a/src/database/__tests__/permissions.test.js b/src/database/__tests__/permissions.test.js index 75b6a57..61e1471 100644 --- a/src/database/__tests__/permissions.test.js +++ b/src/database/__tests__/permissions.test.js @@ -26,4 +26,31 @@ describe('applyPermissionSettingsList', () => { it('returns an empty object when nothing is explicitly set', () => { expect(applyPermissionSettingsList([{ permissions: { user: { info: null } } }])).toEqual({}); }); + + it('applies group permissions first, then user permission settings', () => { + const result = applyPermissionSettingsList([ + { + permissions: { + user: { info: true, edit: false }, + printer: { info: true }, + }, + }, + { + permissions: { + user: { delete: true }, + }, + }, + { + permissions: { + user: { edit: true }, + printer: { info: false }, + }, + }, + ]); + + expect(result).toEqual({ + user: { info: true, edit: true, delete: true }, + printer: { info: false }, + }); + }); }); diff --git a/src/database/database.js b/src/database/database.js index 576541f..12cf402 100644 --- a/src/database/database.js +++ b/src/database/database.js @@ -45,10 +45,13 @@ const NEIGHBORS_CACHE_WINDOW = 25; const NEIGHBORS_CACHE_PREFIX = 'neighbors'; const mergeObjectUpdates = (target, source) => - _.mergeWith(target, source, (objValue, srcValue) => { + _.mergeWith(target, source, (objValue, srcValue, key) => { if (Array.isArray(objValue) || Array.isArray(srcValue)) { return srcValue; } + if (key === 'permissions' && srcValue !== undefined) { + return srcValue; + } }); export const retrieveObjectCache = async ({ model, id, populate = [] }) => { @@ -1021,7 +1024,7 @@ export const getObject = async ({ model, id, populate }) => { return { error: 'Object not found.', code: 404 }; } - const expanded = _.merge(cachedObject || {}, expandObjectIds(result)); + const expanded = mergeObjectUpdates(cachedObject || {}, expandObjectIds(result)); // Update cache with the expanded object await updateObjectCache({ 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', }, diff --git a/src/index.js b/src/index.js index a546fc3..3e31bab 100644 --- a/src/index.js +++ b/src/index.js @@ -7,6 +7,7 @@ import { redisServer } from './database/redis.js'; import { authRoutes, userRoutes, + userGroupRoutes, permissionSettingsRoutes, appPasswordRoutes, fileRoutes, @@ -155,6 +156,7 @@ app.get('/', function (req, res) { app.use('/auth', authRoutes); app.use('/users', userRoutes); +app.use('/usergroups', userGroupRoutes); app.use('/permissionsettings', permissionSettingsRoutes); app.use('/apppasswords', appPasswordRoutes); app.use('/files', fileRoutes); diff --git a/src/routes/index.js b/src/routes/index.js index b6188eb..4c33889 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -1,5 +1,6 @@ import userRoutes from './management/users.js'; -import permissionSettingsRoutes from './management/permissionsettings.js'; +import userGroupRoutes from './management/usergroups.js'; +import permissionSettingsRoutes from './management/permissionsetting.js'; import appPasswordRoutes from './management/apppasswords.js'; import fileRoutes from './management/files.js'; import authRoutes from './misc/auth.js'; @@ -61,6 +62,7 @@ import slicerRoutes from './misc/slicer.js'; export { userRoutes, + userGroupRoutes, permissionSettingsRoutes, appPasswordRoutes, fileRoutes, diff --git a/src/routes/management/permissionsettings.js b/src/routes/management/permissionsetting.js similarity index 98% rename from src/routes/management/permissionsettings.js rename to src/routes/management/permissionsetting.js index f2be443..1aafa10 100644 --- a/src/routes/management/permissionsettings.js +++ b/src/routes/management/permissionsetting.js @@ -19,7 +19,7 @@ import { searchPermissionSettingsRouteHandler, getPermissionSettingsPropertyValuesRouteHandler, getPermissionSettingsNeighborsRouteHandler, -} from '../../services/management/permissionsettings.js'; +} from '../../services/management/permissionsetting.js'; router.get('/', isAuthenticated, async (req, res) => { const { page, limit, property, search, sort, order } = req.query; diff --git a/src/routes/management/usergroups.js b/src/routes/management/usergroups.js new file mode 100644 index 0000000..135c864 --- /dev/null +++ b/src/routes/management/usergroups.js @@ -0,0 +1,99 @@ +import express from 'express'; +import { isAuthenticated } from '../../keycloak.js'; +import { getFilter, convertPropertiesString, getSort } from '../../utils.js'; + +const router = express.Router(); + +const listAllowedFilters = ['name', 'permissionSettings', 'createdAt', 'updatedAt', '_reference']; +const listAllowedSorters = ['name', 'createdAt', '_id', 'updatedAt']; +const propertiesAllowedFilters = ['name']; +import { + listUserGroupsRouteHandler, + getUserGroupRouteHandler, + editUserGroupRouteHandler, + newUserGroupRouteHandler, + deleteUserGroupRouteHandler, + listUserGroupsByPropertiesRouteHandler, + getUserGroupStatsRouteHandler, + getUserGroupHistoryRouteHandler, + searchUserGroupsRouteHandler, + getUserGroupPropertyValuesRouteHandler, + getUserGroupNeighborsRouteHandler, +} from '../../services/management/usergroups.js'; + +router.get('/', isAuthenticated, async (req, res) => { + const { page, limit, property, search, sort, order } = req.query; + const filter = await getFilter(req.query, listAllowedFilters); + listUserGroupsRouteHandler( + req, + res, + page, + limit, + property, + filter, + search, + getSort(sort, listAllowedSorters), + order + ); +}); + +router.get('/properties', isAuthenticated, async (req, res) => { + let properties = convertPropertiesString(req.query.properties); + const filter = await getFilter(req.query, propertiesAllowedFilters, false); + var masterFilter = {}; + if (req.query.masterFilter) { + masterFilter = JSON.parse(req.query.masterFilter); + } + listUserGroupsByPropertiesRouteHandler(req, res, properties, filter, masterFilter); +}); + +router.get('/values', isAuthenticated, async (req, res) => { + const { property } = req.query; + getUserGroupPropertyValuesRouteHandler(req, res, property); +}); + +router.get('/search', isAuthenticated, async (req, res) => { + const { search } = req.query; + searchUserGroupsRouteHandler(req, res, search); +}); + +router.post('/', isAuthenticated, async (req, res) => { + newUserGroupRouteHandler(req, res); +}); + +router.get('/stats', isAuthenticated, async (req, res) => { + getUserGroupStatsRouteHandler(req, res); +}); + +router.get('/history', isAuthenticated, async (req, res) => { + getUserGroupHistoryRouteHandler(req, res); +}); + +router.get('/neighbors', isAuthenticated, async (req, res) => { + const { property, search, sort, order, id } = req.query; + const filter = await getFilter(req.query, listAllowedFilters); + getUserGroupNeighborsRouteHandler( + req, + res, + property, + filter, + search, + getSort(sort, listAllowedSorters), + order, + id + ); +}); + +router.get('/:id', isAuthenticated, async (req, res) => { + getUserGroupRouteHandler(req, res); +}); + +router.put('/:id', isAuthenticated, async (req, res) => { + editUserGroupRouteHandler(req, res); +}); + +router.delete('/:id', isAuthenticated, async (req, res) => { + deleteUserGroupRouteHandler(req, res); +}); + +export default router; diff --git a/src/routes/management/users.js b/src/routes/management/users.js index e1e4119..58f941e 100644 --- a/src/routes/management/users.js +++ b/src/routes/management/users.js @@ -10,6 +10,7 @@ const listAllowedFilters = [ 'firstName', 'lastName', 'email', + 'groups', 'permissionSettings', 'createdAt', 'updatedAt', diff --git a/src/services/management/__tests__/permissionsettings.test.js b/src/services/management/__tests__/permissionsetting.test.js similarity index 89% rename from src/services/management/__tests__/permissionsettings.test.js rename to src/services/management/__tests__/permissionsetting.test.js index 4ae626a..907ea33 100644 --- a/src/services/management/__tests__/permissionsettings.test.js +++ b/src/services/management/__tests__/permissionsetting.test.js @@ -14,8 +14,8 @@ jest.unstable_mockModule('../../../database/database.js', () => ({ getObjectNeighbors: jest.fn(), })); -jest.unstable_mockModule('../../../database/schemas/management/permissionsettings.schema.js', () => ({ - permissionSettingsModel: { modelName: 'PermissionSettings' }, +jest.unstable_mockModule('../../../database/schemas/management/permissionsetting.schema.js', () => ({ + permissionSettingModel: { modelName: 'PermissionSetting' }, })); jest.unstable_mockModule('log4js', () => ({ @@ -35,13 +35,13 @@ const { getPermissionSettingsRouteHandler, newPermissionSettingsRouteHandler, editPermissionSettingsRouteHandler, -} = await import('../permissionsettings.js'); +} = await import('../permissionsetting.js'); const { listObjects, getObject, editObject, newObject } = await import( '../../../database/database.js' ); -const { permissionSettingsModel } = await import( - '../../../database/schemas/management/permissionsettings.schema.js' +const { permissionSettingModel } = await import( + '../../../database/schemas/management/permissionsetting.schema.js' ); describe('Permission Settings Service Route Handlers', () => { @@ -69,7 +69,7 @@ describe('Permission Settings Service Route Handlers', () => { await listPermissionSettingsRouteHandler(req, res); expect(listObjects).toHaveBeenCalledWith( - expect.objectContaining({ model: permissionSettingsModel }) + expect.objectContaining({ model: permissionSettingModel }) ); expect(res.send).toHaveBeenCalledWith(mockResult); }); @@ -112,7 +112,7 @@ describe('Permission Settings Service Route Handlers', () => { expect(getObject).toHaveBeenCalledWith( expect.objectContaining({ - model: permissionSettingsModel, + model: permissionSettingModel, id: '123', }) ); diff --git a/src/services/management/__tests__/usergroups.test.js b/src/services/management/__tests__/usergroups.test.js new file mode 100644 index 0000000..3e84651 --- /dev/null +++ b/src/services/management/__tests__/usergroups.test.js @@ -0,0 +1,130 @@ +import { jest } from '@jest/globals'; + +jest.unstable_mockModule('../../../database/database.js', () => ({ + searchObjects: jest.fn(), + getPropertyValues: jest.fn(), + listObjects: jest.fn(), + getObject: jest.fn(), + editObject: jest.fn(), + newObject: jest.fn(), + deleteObject: jest.fn(), + listObjectsByProperties: jest.fn(), + getModelStats: jest.fn(), + getModelHistory: jest.fn(), + getObjectNeighbors: jest.fn(), +})); + +jest.unstable_mockModule('../../../database/schemas/management/usergroup.schema.js', () => ({ + userGroupModel: { modelName: 'UserGroup' }, +})); + +jest.unstable_mockModule('log4js', () => ({ + default: { + getLogger: () => ({ + level: 'info', + debug: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + trace: jest.fn(), + }), + }, +})); + +const { + listUserGroupsRouteHandler, + getUserGroupRouteHandler, + newUserGroupRouteHandler, + editUserGroupRouteHandler, +} = await import('../usergroups.js'); + +const { listObjects, getObject, editObject, newObject } = await import( + '../../../database/database.js' +); +const { userGroupModel } = await import( + '../../../database/schemas/management/usergroup.schema.js' +); + +describe('User Groups Service Route Handlers', () => { + let req, res; + + beforeEach(() => { + req = { + params: {}, + query: {}, + body: {}, + user: { id: 'test-user-id' }, + }; + res = { + send: jest.fn(), + status: jest.fn().mockReturnThis(), + }; + jest.clearAllMocks(); + }); + + describe('listUserGroupsRouteHandler', () => { + it('should list user groups', async () => { + const mockResult = [{ _id: '1', name: 'Admins' }]; + listObjects.mockResolvedValue(mockResult); + + await listUserGroupsRouteHandler(req, res); + + expect(listObjects).toHaveBeenCalledWith( + expect.objectContaining({ model: userGroupModel }) + ); + expect(res.send).toHaveBeenCalledWith(mockResult); + }); + }); + + describe('newUserGroupRouteHandler', () => { + it('should create a new user group', async () => { + req.body = { name: 'Editors', permissionSettings: [{ _id: 'pms-1' }] }; + const mockUserGroup = { _id: '456', ...req.body }; + newObject.mockResolvedValue(mockUserGroup); + + await newUserGroupRouteHandler(req, res); + + expect(newObject).toHaveBeenCalledWith( + expect.objectContaining({ + model: userGroupModel, + newData: expect.objectContaining({ + name: 'Editors', + permissionSettings: ['pms-1'], + }), + }) + ); + expect(res.send).toHaveBeenCalledWith(mockUserGroup); + }); + }); + + describe('editUserGroupRouteHandler', () => { + it('should update a user group', async () => { + req.params.id = '507f1f77bcf86cd799439011'; + req.body = { name: 'Editors', permissionSettings: ['pms-1'] }; + const mockResult = { _id: '507f1f77bcf86cd799439011', ...req.body }; + editObject.mockResolvedValue(mockResult); + + await editUserGroupRouteHandler(req, res); + + expect(editObject).toHaveBeenCalled(); + expect(res.send).toHaveBeenCalledWith(mockResult); + }); + }); + + describe('getUserGroupRouteHandler', () => { + it('should get a user group by id', async () => { + req.params.id = '123'; + const mockResult = { _id: '123', name: 'Admins' }; + getObject.mockResolvedValue(mockResult); + + await getUserGroupRouteHandler(req, res); + + expect(getObject).toHaveBeenCalledWith( + expect.objectContaining({ + model: userGroupModel, + id: '123', + }) + ); + expect(res.send).toHaveBeenCalledWith(mockResult); + }); + }); +}); diff --git a/src/services/management/permissionsettings.js b/src/services/management/permissionsetting.js similarity index 89% rename from src/services/management/permissionsettings.js rename to src/services/management/permissionsetting.js index 58e8c5a..83ad134 100644 --- a/src/services/management/permissionsettings.js +++ b/src/services/management/permissionsetting.js @@ -1,5 +1,5 @@ import config from '../../config.js'; -import { permissionSettingsModel } from '../../database/schemas/management/permissionsettings.schema.js'; +import { permissionSettingModel } from '../../database/schemas/management/permissionsetting.schema.js'; import log4js from 'log4js'; import mongoose from 'mongoose'; import { @@ -16,7 +16,7 @@ import { getObjectNeighbors, } from '../../database/database.js'; -const logger = log4js.getLogger('PermissionSettings'); +const logger = log4js.getLogger('PermissionSetting'); logger.level = config.server.logLevel; export const listPermissionSettingsRouteHandler = async ( @@ -31,7 +31,7 @@ export const listPermissionSettingsRouteHandler = async ( order = 'ascend' ) => { const result = await listObjects({ - model: permissionSettingsModel, + model: permissionSettingModel, page, limit, property, @@ -61,7 +61,7 @@ export const listPermissionSettingsByPropertiesRouteHandler = async ( masterFilter = {} ) => { const result = await listObjectsByProperties({ - model: permissionSettingsModel, + model: permissionSettingModel, properties, filter, masterFilter, @@ -79,7 +79,7 @@ export const listPermissionSettingsByPropertiesRouteHandler = async ( export const getPermissionSettingsPropertyValuesRouteHandler = async (req, res, property) => { const result = await getPropertyValues({ - model: permissionSettingsModel, + model: permissionSettingModel, property, }); res.send(result); @@ -87,7 +87,7 @@ export const getPermissionSettingsPropertyValuesRouteHandler = async (req, res, export const searchPermissionSettingsRouteHandler = async (req, res, search) => { const result = await searchObjects({ - model: permissionSettingsModel, + model: permissionSettingModel, search, }); res.send(result); @@ -96,7 +96,7 @@ export const searchPermissionSettingsRouteHandler = async (req, res, search) => export const getPermissionSettingsRouteHandler = async (req, res) => { const id = req.params.id; const result = await getObject({ - model: permissionSettingsModel, + model: permissionSettingModel, id, }); if (result?.error) { @@ -118,7 +118,7 @@ export const editPermissionSettingsRouteHandler = async (req, res) => { permissions: req.body.permissions, }; const result = await editObject({ - model: permissionSettingsModel, + model: permissionSettingModel, id, updateData, user: req.user, @@ -142,7 +142,7 @@ export const newPermissionSettingsRouteHandler = async (req, res) => { permissions: req.body.permissions, }; const result = await newObject({ - model: permissionSettingsModel, + model: permissionSettingModel, newData, user: req.user, }); @@ -162,7 +162,7 @@ export const deletePermissionSettingsRouteHandler = async (req, res) => { logger.trace(`Permission settings with ID: ${id}`); const result = await deleteObject({ - model: permissionSettingsModel, + model: permissionSettingModel, id, user: req.user, }); @@ -177,7 +177,7 @@ export const deletePermissionSettingsRouteHandler = async (req, res) => { }; export const getPermissionSettingsStatsRouteHandler = async (req, res) => { - const result = await getModelStats({ model: permissionSettingsModel }); + const result = await getModelStats({ model: permissionSettingModel }); if (result?.error) { logger.error('Error fetching permission settings stats:', result.error); return res.status(result.code).send(result); @@ -189,7 +189,7 @@ export const getPermissionSettingsStatsRouteHandler = async (req, res) => { export const getPermissionSettingsHistoryRouteHandler = async (req, res) => { const from = req.query.from; const to = req.query.to; - const result = await getModelHistory({ model: permissionSettingsModel, from, to }); + const result = await getModelHistory({ model: permissionSettingModel, from, to }); if (result?.error) { logger.error('Error fetching permission settings history:', result.error); return res.status(result.code).send(result); @@ -213,7 +213,7 @@ export const getPermissionSettingsNeighborsRouteHandler = async ( } const result = await getObjectNeighbors({ - model: permissionSettingsModel, + model: permissionSettingModel, id, filter, search, diff --git a/src/services/management/usergroups.js b/src/services/management/usergroups.js new file mode 100644 index 0000000..7edafb5 --- /dev/null +++ b/src/services/management/usergroups.js @@ -0,0 +1,240 @@ +import config from '../../config.js'; +import { userGroupModel } from '../../database/schemas/management/usergroup.schema.js'; +import log4js from 'log4js'; +import mongoose from 'mongoose'; +import { + deleteObject, + listObjects, + getObject, + editObject, + newObject, + listObjectsByProperties, + getModelStats, + getModelHistory, + searchObjects, + getPropertyValues, + getObjectNeighbors, +} from '../../database/database.js'; + +const logger = log4js.getLogger('UserGroups'); +logger.level = config.server.logLevel; + +const USER_GROUP_POPULATE = ['permissionSettings']; + +const toIdList = (value) => + Array.isArray(value) ? value.map((item) => item?._id || item) : value; + +export const listUserGroupsRouteHandler = async ( + req, + res, + page = 1, + limit = 25, + property = '', + filter = {}, + search = '', + sort = '', + order = 'ascend' +) => { + const result = await listObjects({ + model: userGroupModel, + page, + limit, + property, + filter, + search, + sort, + order, + populate: USER_GROUP_POPULATE, + }); + + if (result?.error) { + logger.error('Error listing user groups.'); + res.status(result.code).send(result); + return; + } + + logger.debug( + `List of user groups (Page ${page}, Limit ${limit}). Count: ${result.length}.` + ); + res.send(result); +}; + +export const listUserGroupsByPropertiesRouteHandler = async ( + req, + res, + properties = '', + filter = {}, + masterFilter = {} +) => { + const result = await listObjectsByProperties({ + model: userGroupModel, + properties, + filter, + masterFilter, + }); + + if (result?.error) { + logger.error('Error listing user groups.'); + res.status(result.code).send(result); + return; + } + + logger.debug(`List of user groups. Count: ${result.length}`); + res.send(result); +}; + +export const getUserGroupPropertyValuesRouteHandler = async (req, res, property) => { + const result = await getPropertyValues({ + model: userGroupModel, + property, + }); + res.send(result); +}; + +export const searchUserGroupsRouteHandler = async (req, res, search) => { + const result = await searchObjects({ + model: userGroupModel, + search, + }); + res.send(result); +}; + +export const getUserGroupRouteHandler = async (req, res) => { + const id = req.params.id; + const result = await getObject({ + model: userGroupModel, + id, + populate: USER_GROUP_POPULATE, + }); + if (result?.error) { + logger.warn(`User group not found with supplied id.`); + return res.status(result.code).send(result); + } + logger.debug(`Retreived user group with ID: ${id}`); + res.send(result); +}; + +export const editUserGroupRouteHandler = async (req, res) => { + const id = new mongoose.Types.ObjectId(req.params.id); + + logger.trace(`User group with ID: ${id}`); + + const updateData = { + updatedAt: new Date(), + name: req.body.name, + permissionSettings: toIdList(req.body.permissionSettings), + }; + const result = await editObject({ + model: userGroupModel, + id, + updateData, + user: req.user, + populate: USER_GROUP_POPULATE, + }); + + if (result.error) { + logger.error('Error editing user group:', result.error); + res.status(result).send(result); + return; + } + + logger.debug(`Edited user group with ID: ${id}`); + + res.send(result); +}; + +export const newUserGroupRouteHandler = async (req, res) => { + const newData = { + updatedAt: new Date(), + name: req.body.name, + permissionSettings: toIdList(req.body.permissionSettings), + permissions: req.body.permissions, + }; + const result = await newObject({ + model: userGroupModel, + newData, + user: req.user, + }); + if (result.error) { + logger.error('No user group created:', result.error); + return res.status(result.code).send(result); + } + + logger.debug(`New user group with ID: ${result._id}`); + + res.send(result); +}; + +export const deleteUserGroupRouteHandler = async (req, res) => { + const id = new mongoose.Types.ObjectId(req.params.id); + + logger.trace(`User group with ID: ${id}`); + + const result = await deleteObject({ + model: userGroupModel, + id, + user: req.user, + }); + if (result.error) { + logger.error('No user group deleted:', result.error); + return res.status(result.code).send(result); + } + + logger.debug(`Deleted user group with ID: ${result._id}`); + + res.send(result); +}; + +export const getUserGroupStatsRouteHandler = async (req, res) => { + const result = await getModelStats({ model: userGroupModel }); + if (result?.error) { + logger.error('Error fetching user group stats:', result.error); + return res.status(result.code).send(result); + } + logger.trace('User group stats:', result); + res.send(result); +}; + +export const getUserGroupHistoryRouteHandler = async (req, res) => { + const from = req.query.from; + const to = req.query.to; + const result = await getModelHistory({ model: userGroupModel, from, to }); + if (result?.error) { + logger.error('Error fetching user group history:', result.error); + return res.status(result.code).send(result); + } + logger.trace('User group history:', result); + res.send(result); +}; + +export const getUserGroupNeighborsRouteHandler = async ( + req, + res, + property = '', + filter = {}, + search = '', + sort = '', + order = 'ascend', + id +) => { + if (!id) { + return res.status(400).send({ error: 'Missing id parameter', code: 400 }); + } + + const result = await getObjectNeighbors({ + model: userGroupModel, + id, + filter, + search, + sort, + order, + }); + + if (result?.error) { + logger.error('Error fetching userGroup neighbors.'); + return res.status(result.code).send(result); + } + + logger.debug(`Retrieved userGroup neighbors for ID: ${id}`); + res.send(result); +}; diff --git a/src/services/management/users.js b/src/services/management/users.js index 01bbecf..72c8c6a 100644 --- a/src/services/management/users.js +++ b/src/services/management/users.js @@ -19,7 +19,7 @@ import { const logger = log4js.getLogger('Users'); logger.level = config.server.logLevel; -const USER_POPULATE = ['profileImage', 'permissionSettings']; +const USER_POPULATE = ['profileImage', 'permissionSettings', 'groups']; export const listUsersRouteHandler = async ( req, @@ -122,6 +122,9 @@ export const editUserRouteHandler = async (req, res) => { lastName: req.body.lastName, email: req.body.email, profileImage: req.body.profileImage, + groups: Array.isArray(req.body.groups) + ? req.body.groups.map((item) => item?._id || item) + : req.body.groups, permissionSettings: Array.isArray(req.body.permissionSettings) ? req.body.permissionSettings.map((item) => item?._id || item) : req.body.permissionSettings,