Refactor permission settings and introduce user group management
All checks were successful
farmcontrol/farmcontrol-ws/pipeline/head This commit looks good

- Replaced the existing permission settings schema with a new permission setting schema, enhancing the management of user permissions.
- Updated user and user group schemas to include references to the new permission setting model, allowing for more flexible permission handling.
- Added utility functions for resolving referenced documents and recalculating permissions, improving the overall permission management process.
- Removed the old permission settings schema to streamline the codebase and reduce redundancy.
This commit is contained in:
Tom Butcher 2026-08-20 15:04:55 +01:00
parent 3742839737
commit 998bb725a2
7 changed files with 197 additions and 85 deletions

View File

@ -22,6 +22,16 @@ const cacheLogger = log4js.getLogger('Local Cache');
logger.level = config.server.logLevel; logger.level = config.server.logLevel;
cacheLogger.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) // Default cache TTL in seconds (similar to previous in-memory cache)
const CACHE_TTL_SECONDS = config.database?.redis?.ttlSeconds || 5; const CACHE_TTL_SECONDS = config.database?.redis?.ttlSeconds || 5;
@ -69,13 +79,13 @@ export const updateObjectCache = async ({
for (const key of matchingKeys) { for (const key of matchingKeys) {
logger.trace('Updating object cache:', key); logger.trace('Updating object cache:', key);
const cachedObject = (await redisServer.getKey(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); await redisServer.setKey(key, mergedObject, CACHE_TTL_SECONDS);
mergedObjects.push(mergedObject); mergedObjects.push(mergedObject);
} }
const cacheObject = (await redisServer.getKey(cacheKey)) || {}; const cacheObject = (await redisServer.getKey(cacheKey)) || {};
const mergedObject = _.merge(cacheObject, object); const mergedObject = mergeObjectUpdates(cacheObject, object);
await redisServer.setKey(cacheKey, mergedObject, CACHE_TTL_SECONDS); await redisServer.setKey(cacheKey, mergedObject, CACHE_TTL_SECONDS);
cacheLogger.trace('Updated:', { cacheLogger.trace('Updated:', {

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',
}, },