Add user group management functionality and refactor permission settings
All checks were successful
farmcontrol/farmcontrol-api/pipeline/head This commit looks good

This commit introduces a new feature for managing user groups within the application, including the creation of a `usergroup.schema.js` file for the Mongoose schema and corresponding route handlers for CRUD operations. Additionally, the permission settings functionality has been refactored to use a singular `permissionSetting` model instead of the previous plural form. Tests have been added to ensure the functionality of both user groups and permission settings services, enhancing the application's capability to manage user roles and permissions effectively.
This commit is contained in:
Tom Butcher 2026-08-20 15:04:52 +01:00
parent 806a32d306
commit 1c51766da6
18 changed files with 717 additions and 108 deletions

View File

@ -26,4 +26,31 @@ describe('applyPermissionSettingsList', () => {
it('returns an empty object when nothing is explicitly set', () => { it('returns an empty object when nothing is explicitly set', () => {
expect(applyPermissionSettingsList([{ permissions: { user: { info: null } } }])).toEqual({}); 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 },
});
});
}); });

View File

@ -45,10 +45,13 @@ const NEIGHBORS_CACHE_WINDOW = 25;
const NEIGHBORS_CACHE_PREFIX = 'neighbors'; const NEIGHBORS_CACHE_PREFIX = 'neighbors';
const mergeObjectUpdates = (target, source) => const mergeObjectUpdates = (target, source) =>
_.mergeWith(target, source, (objValue, srcValue) => { _.mergeWith(target, source, (objValue, srcValue, key) => {
if (Array.isArray(objValue) || Array.isArray(srcValue)) { if (Array.isArray(objValue) || Array.isArray(srcValue)) {
return srcValue; return srcValue;
} }
if (key === 'permissions' && srcValue !== undefined) {
return srcValue;
}
}); });
export const retrieveObjectCache = async ({ model, id, populate = [] }) => { 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 }; 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 // Update cache with the expanded object
await updateObjectCache({ await updateObjectCache({

View File

@ -1,3 +1,5 @@
import mongoose from 'mongoose';
export const applyPermissionSettingsList = (settingsList = []) => { export const applyPermissionSettingsList = (settingsList = []) => {
const permissions = {}; const permissions = {};
@ -36,3 +38,25 @@ export const getPermissionSettingsId = (value) => {
} }
return 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);
};

View File

@ -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
);

View File

@ -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
);

View File

@ -1,6 +1,10 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { generateId } from '../../utils.js'; import { generateId } from '../../utils.js';
import { applyPermissionSettingsList, getPermissionSettingsId } from '../../permissions.js'; import {
applyPermissionSettingsList,
resolvePermissionSettings,
resolveReferencedDocs,
} from '../../permissions.js';
const { Schema } = mongoose; const { Schema } = mongoose;
@ -14,8 +18,9 @@ const userSchema = new mongoose.Schema(
email: { required: true, type: String }, email: { required: true, type: String },
profileImage: { type: mongoose.SchemaTypes.ObjectId, ref: 'file', required: false }, profileImage: { type: mongoose.SchemaTypes.ObjectId, ref: 'file', required: false },
appPasswordHash: { type: String, required: false, select: false }, appPasswordHash: { type: String, required: false, select: false },
groups: [{ type: Schema.Types.ObjectId, ref: 'userGroup', required: false }],
permissionSettings: [ 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: () => ({}) }, permissions: { type: Schema.Types.Mixed, required: false, default: () => ({}) },
}, },
@ -30,30 +35,18 @@ userSchema.virtual('id').get(function () {
userSchema.set('toJSON', { virtuals: true }); 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) { userSchema.statics.recalculate = async function (user, actingUser) {
const userId = user?._id || user; const userId = user?._id || user;
if (!userId) { if (!userId) {
return; return;
} }
const groups = await resolveReferencedDocs('userGroup', user?.groups);
const settings = await resolvePermissionSettings(user); 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'); const { editObject } = await import('../../database.js');
await editObject({ await editObject({
@ -61,7 +54,7 @@ userSchema.statics.recalculate = async function (user, actingUser) {
id: userId, id: userId,
updateData: { permissions }, updateData: { permissions },
user: actingUser, user: actingUser,
populate: ['profileImage', 'permissionSettings'], populate: ['profileImage', 'permissionSettings', 'groups'],
recalculate: false, recalculate: false,
}); });
}; };

View File

@ -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);

View File

@ -24,7 +24,8 @@ import { stockLocationModel } from './inventory/stocklocation.schema.js';
import { stockTransferModel } from './inventory/stocktransfer.schema.js'; import { stockTransferModel } from './inventory/stocktransfer.schema.js';
import { auditLogModel } from './management/auditlog.schema.js'; import { auditLogModel } from './management/auditlog.schema.js';
import { userModel } from './management/user.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 { appPasswordModel } from './management/apppassword.schema.js';
import { noteTypeModel } from './management/notetype.schema.js'; import { noteTypeModel } from './management/notetype.schema.js';
import { noteModel } from './misc/note.schema.js'; import { noteModel } from './misc/note.schema.js';
@ -213,10 +214,17 @@ export const models = {
referenceField: '_reference', referenceField: '_reference',
label: 'User', label: 'User',
}, },
PMS: { UGP: {
model: permissionSettingsModel, model: userGroupModel,
idField: '_id', idField: '_id',
type: 'permissionSettings', type: 'userGroup',
referenceField: '_reference',
label: 'User Group',
},
PMS: {
model: permissionSettingModel,
idField: '_id',
type: 'permissionSetting',
referenceField: '_reference', referenceField: '_reference',
label: 'Permission Settings', label: 'Permission Settings',
}, },

View File

@ -7,6 +7,7 @@ import { redisServer } from './database/redis.js';
import { import {
authRoutes, authRoutes,
userRoutes, userRoutes,
userGroupRoutes,
permissionSettingsRoutes, permissionSettingsRoutes,
appPasswordRoutes, appPasswordRoutes,
fileRoutes, fileRoutes,
@ -155,6 +156,7 @@ app.get('/', function (req, res) {
app.use('/auth', authRoutes); app.use('/auth', authRoutes);
app.use('/users', userRoutes); app.use('/users', userRoutes);
app.use('/usergroups', userGroupRoutes);
app.use('/permissionsettings', permissionSettingsRoutes); app.use('/permissionsettings', permissionSettingsRoutes);
app.use('/apppasswords', appPasswordRoutes); app.use('/apppasswords', appPasswordRoutes);
app.use('/files', fileRoutes); app.use('/files', fileRoutes);

View File

@ -1,5 +1,6 @@
import userRoutes from './management/users.js'; 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 appPasswordRoutes from './management/apppasswords.js';
import fileRoutes from './management/files.js'; import fileRoutes from './management/files.js';
import authRoutes from './misc/auth.js'; import authRoutes from './misc/auth.js';
@ -61,6 +62,7 @@ import slicerRoutes from './misc/slicer.js';
export { export {
userRoutes, userRoutes,
userGroupRoutes,
permissionSettingsRoutes, permissionSettingsRoutes,
appPasswordRoutes, appPasswordRoutes,
fileRoutes, fileRoutes,

View File

@ -19,7 +19,7 @@ import {
searchPermissionSettingsRouteHandler, searchPermissionSettingsRouteHandler,
getPermissionSettingsPropertyValuesRouteHandler, getPermissionSettingsPropertyValuesRouteHandler,
getPermissionSettingsNeighborsRouteHandler, getPermissionSettingsNeighborsRouteHandler,
} from '../../services/management/permissionsettings.js'; } from '../../services/management/permissionsetting.js';
router.get('/', isAuthenticated, async (req, res) => { router.get('/', isAuthenticated, async (req, res) => {
const { page, limit, property, search, sort, order } = req.query; const { page, limit, property, search, sort, order } = req.query;

View File

@ -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;

View File

@ -10,6 +10,7 @@ const listAllowedFilters = [
'firstName', 'firstName',
'lastName', 'lastName',
'email', 'email',
'groups',
'permissionSettings', 'permissionSettings',
'createdAt', 'createdAt',
'updatedAt', 'updatedAt',

View File

@ -14,8 +14,8 @@ jest.unstable_mockModule('../../../database/database.js', () => ({
getObjectNeighbors: jest.fn(), getObjectNeighbors: jest.fn(),
})); }));
jest.unstable_mockModule('../../../database/schemas/management/permissionsettings.schema.js', () => ({ jest.unstable_mockModule('../../../database/schemas/management/permissionsetting.schema.js', () => ({
permissionSettingsModel: { modelName: 'PermissionSettings' }, permissionSettingModel: { modelName: 'PermissionSetting' },
})); }));
jest.unstable_mockModule('log4js', () => ({ jest.unstable_mockModule('log4js', () => ({
@ -35,13 +35,13 @@ const {
getPermissionSettingsRouteHandler, getPermissionSettingsRouteHandler,
newPermissionSettingsRouteHandler, newPermissionSettingsRouteHandler,
editPermissionSettingsRouteHandler, editPermissionSettingsRouteHandler,
} = await import('../permissionsettings.js'); } = await import('../permissionsetting.js');
const { listObjects, getObject, editObject, newObject } = await import( const { listObjects, getObject, editObject, newObject } = await import(
'../../../database/database.js' '../../../database/database.js'
); );
const { permissionSettingsModel } = await import( const { permissionSettingModel } = await import(
'../../../database/schemas/management/permissionsettings.schema.js' '../../../database/schemas/management/permissionsetting.schema.js'
); );
describe('Permission Settings Service Route Handlers', () => { describe('Permission Settings Service Route Handlers', () => {
@ -69,7 +69,7 @@ describe('Permission Settings Service Route Handlers', () => {
await listPermissionSettingsRouteHandler(req, res); await listPermissionSettingsRouteHandler(req, res);
expect(listObjects).toHaveBeenCalledWith( expect(listObjects).toHaveBeenCalledWith(
expect.objectContaining({ model: permissionSettingsModel }) expect.objectContaining({ model: permissionSettingModel })
); );
expect(res.send).toHaveBeenCalledWith(mockResult); expect(res.send).toHaveBeenCalledWith(mockResult);
}); });
@ -112,7 +112,7 @@ describe('Permission Settings Service Route Handlers', () => {
expect(getObject).toHaveBeenCalledWith( expect(getObject).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
model: permissionSettingsModel, model: permissionSettingModel,
id: '123', id: '123',
}) })
); );

View File

@ -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);
});
});
});

View File

@ -1,5 +1,5 @@
import config from '../../config.js'; 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 log4js from 'log4js';
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { import {
@ -16,7 +16,7 @@ import {
getObjectNeighbors, getObjectNeighbors,
} from '../../database/database.js'; } from '../../database/database.js';
const logger = log4js.getLogger('PermissionSettings'); const logger = log4js.getLogger('PermissionSetting');
logger.level = config.server.logLevel; logger.level = config.server.logLevel;
export const listPermissionSettingsRouteHandler = async ( export const listPermissionSettingsRouteHandler = async (
@ -31,7 +31,7 @@ export const listPermissionSettingsRouteHandler = async (
order = 'ascend' order = 'ascend'
) => { ) => {
const result = await listObjects({ const result = await listObjects({
model: permissionSettingsModel, model: permissionSettingModel,
page, page,
limit, limit,
property, property,
@ -61,7 +61,7 @@ export const listPermissionSettingsByPropertiesRouteHandler = async (
masterFilter = {} masterFilter = {}
) => { ) => {
const result = await listObjectsByProperties({ const result = await listObjectsByProperties({
model: permissionSettingsModel, model: permissionSettingModel,
properties, properties,
filter, filter,
masterFilter, masterFilter,
@ -79,7 +79,7 @@ export const listPermissionSettingsByPropertiesRouteHandler = async (
export const getPermissionSettingsPropertyValuesRouteHandler = async (req, res, property) => { export const getPermissionSettingsPropertyValuesRouteHandler = async (req, res, property) => {
const result = await getPropertyValues({ const result = await getPropertyValues({
model: permissionSettingsModel, model: permissionSettingModel,
property, property,
}); });
res.send(result); res.send(result);
@ -87,7 +87,7 @@ export const getPermissionSettingsPropertyValuesRouteHandler = async (req, res,
export const searchPermissionSettingsRouteHandler = async (req, res, search) => { export const searchPermissionSettingsRouteHandler = async (req, res, search) => {
const result = await searchObjects({ const result = await searchObjects({
model: permissionSettingsModel, model: permissionSettingModel,
search, search,
}); });
res.send(result); res.send(result);
@ -96,7 +96,7 @@ export const searchPermissionSettingsRouteHandler = async (req, res, search) =>
export const getPermissionSettingsRouteHandler = async (req, res) => { export const getPermissionSettingsRouteHandler = async (req, res) => {
const id = req.params.id; const id = req.params.id;
const result = await getObject({ const result = await getObject({
model: permissionSettingsModel, model: permissionSettingModel,
id, id,
}); });
if (result?.error) { if (result?.error) {
@ -118,7 +118,7 @@ export const editPermissionSettingsRouteHandler = async (req, res) => {
permissions: req.body.permissions, permissions: req.body.permissions,
}; };
const result = await editObject({ const result = await editObject({
model: permissionSettingsModel, model: permissionSettingModel,
id, id,
updateData, updateData,
user: req.user, user: req.user,
@ -142,7 +142,7 @@ export const newPermissionSettingsRouteHandler = async (req, res) => {
permissions: req.body.permissions, permissions: req.body.permissions,
}; };
const result = await newObject({ const result = await newObject({
model: permissionSettingsModel, model: permissionSettingModel,
newData, newData,
user: req.user, user: req.user,
}); });
@ -162,7 +162,7 @@ export const deletePermissionSettingsRouteHandler = async (req, res) => {
logger.trace(`Permission settings with ID: ${id}`); logger.trace(`Permission settings with ID: ${id}`);
const result = await deleteObject({ const result = await deleteObject({
model: permissionSettingsModel, model: permissionSettingModel,
id, id,
user: req.user, user: req.user,
}); });
@ -177,7 +177,7 @@ export const deletePermissionSettingsRouteHandler = async (req, res) => {
}; };
export const getPermissionSettingsStatsRouteHandler = async (req, res) => { export const getPermissionSettingsStatsRouteHandler = async (req, res) => {
const result = await getModelStats({ model: permissionSettingsModel }); const result = await getModelStats({ model: permissionSettingModel });
if (result?.error) { if (result?.error) {
logger.error('Error fetching permission settings stats:', result.error); logger.error('Error fetching permission settings stats:', result.error);
return res.status(result.code).send(result); return res.status(result.code).send(result);
@ -189,7 +189,7 @@ export const getPermissionSettingsStatsRouteHandler = async (req, res) => {
export const getPermissionSettingsHistoryRouteHandler = async (req, res) => { export const getPermissionSettingsHistoryRouteHandler = async (req, res) => {
const from = req.query.from; const from = req.query.from;
const to = req.query.to; const to = req.query.to;
const result = await getModelHistory({ model: permissionSettingsModel, from, to }); const result = await getModelHistory({ model: permissionSettingModel, from, to });
if (result?.error) { if (result?.error) {
logger.error('Error fetching permission settings history:', result.error); logger.error('Error fetching permission settings history:', result.error);
return res.status(result.code).send(result); return res.status(result.code).send(result);
@ -213,7 +213,7 @@ export const getPermissionSettingsNeighborsRouteHandler = async (
} }
const result = await getObjectNeighbors({ const result = await getObjectNeighbors({
model: permissionSettingsModel, model: permissionSettingModel,
id, id,
filter, filter,
search, search,

View File

@ -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);
};

View File

@ -19,7 +19,7 @@ import {
const logger = log4js.getLogger('Users'); const logger = log4js.getLogger('Users');
logger.level = config.server.logLevel; logger.level = config.server.logLevel;
const USER_POPULATE = ['profileImage', 'permissionSettings']; const USER_POPULATE = ['profileImage', 'permissionSettings', 'groups'];
export const listUsersRouteHandler = async ( export const listUsersRouteHandler = async (
req, req,
@ -122,6 +122,9 @@ export const editUserRouteHandler = async (req, res) => {
lastName: req.body.lastName, lastName: req.body.lastName,
email: req.body.email, email: req.body.email,
profileImage: req.body.profileImage, 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) permissionSettings: Array.isArray(req.body.permissionSettings)
? req.body.permissionSettings.map((item) => item?._id || item) ? req.body.permissionSettings.map((item) => item?._id || item)
: req.body.permissionSettings, : req.body.permissionSettings,