diff --git a/backend/src/db/api/activity_feed_items.js b/backend/src/db/api/activity_feed_items.js index b53ed65..518a8c0 100644 --- a/backend/src/db/api/activity_feed_items.js +++ b/backend/src/db/api/activity_feed_items.js @@ -348,6 +348,10 @@ module.exports = class Activity_feed_itemsDBApi { where.organizationsId = options.currentUser.organizationsId; } } + + if (!globalAccess && options?.currentUser?.tenantId) { + where.tenantId = options.currentUser.tenantId; + } offset = currentPage * limit; diff --git a/backend/src/db/api/form_field_choices.js b/backend/src/db/api/form_field_choices.js index 82d68aa..aa6e385 100644 --- a/backend/src/db/api/form_field_choices.js +++ b/backend/src/db/api/form_field_choices.js @@ -298,6 +298,10 @@ module.exports = class Form_field_choicesDBApi { where.organizationsId = options.currentUser.organizationsId; } } + + if (!globalAccess && options?.currentUser?.tenantId) { + where.tenantId = options.currentUser.tenantId; + } offset = currentPage * limit; diff --git a/backend/src/db/api/form_fields.js b/backend/src/db/api/form_fields.js index f33543c..efff798 100644 --- a/backend/src/db/api/form_fields.js +++ b/backend/src/db/api/form_fields.js @@ -362,6 +362,10 @@ module.exports = class Form_fieldsDBApi { where.organizationsId = options.currentUser.organizationsId; } } + + if (!globalAccess && options?.currentUser?.tenantId) { + where.tenantId = options.currentUser.tenantId; + } offset = currentPage * limit; diff --git a/backend/src/db/api/form_submissions.js b/backend/src/db/api/form_submissions.js index 04ca85b..2ec268c 100644 --- a/backend/src/db/api/form_submissions.js +++ b/backend/src/db/api/form_submissions.js @@ -412,6 +412,10 @@ module.exports = class Form_submissionsDBApi { where.organizationsId = options.currentUser.organizationsId; } } + + if (!globalAccess && options?.currentUser?.tenantId) { + where.tenantId = options.currentUser.tenantId; + } offset = currentPage * limit; diff --git a/backend/src/db/api/form_templates.js b/backend/src/db/api/form_templates.js index 2b5dc55..82c58eb 100644 --- a/backend/src/db/api/form_templates.js +++ b/backend/src/db/api/form_templates.js @@ -314,6 +314,10 @@ module.exports = class Form_templatesDBApi { where.organizationsId = options.currentUser.organizationsId; } } + + if (!globalAccess && options?.currentUser?.tenantId) { + where.tenantId = options.currentUser.tenantId; + } offset = currentPage * limit; diff --git a/backend/src/db/api/locations.js b/backend/src/db/api/locations.js index 080c702..c46ea4d 100644 --- a/backend/src/db/api/locations.js +++ b/backend/src/db/api/locations.js @@ -349,6 +349,10 @@ module.exports = class LocationsDBApi { where.organizationsId = options.currentUser.organizationsId; } } + + if (!globalAccess && options?.currentUser?.tenantId) { + where.tenantId = options.currentUser.tenantId; + } offset = currentPage * limit; diff --git a/backend/src/db/api/organizations.js b/backend/src/db/api/organizations.js index a991ba1..7b0420b 100644 --- a/backend/src/db/api/organizations.js +++ b/backend/src/db/api/organizations.js @@ -1,4 +1,3 @@ - const db = require('../models'); const FileDBApi = require('./file'); const crypto = require('crypto'); @@ -25,6 +24,12 @@ module.exports = class OrganizationsDBApi { || null , + address: data.address || null, + city: data.city || null, + state: data.state || null, + country: data.country || null, + zip: data.zip || null, + navOrientation: data.navOrientation || 'side', importHash: data.importHash || null, createdById: currentUser.id, @@ -55,7 +60,13 @@ module.exports = class OrganizationsDBApi { || null , - + address: item.address || null, + city: item.city || null, + state: item.state || null, + country: item.country || null, + zip: item.zip || null, + navOrientation: item.navOrientation || 'side', + importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, @@ -74,9 +85,9 @@ module.exports = class OrganizationsDBApi { static async update(id, data, options) { const currentUser = (options && options.currentUser) || {id: null}; const transaction = (options && options.transaction) || undefined; - const globalAccess = currentUser.app_role?.globalAccess; + // const globalAccess = currentUser.app_role?.globalAccess; - const organizations = await db.organizations.findByPk(id, {}, {transaction}); + const organizations = await db.organizations.findByPk(id, { transaction }); @@ -84,6 +95,12 @@ module.exports = class OrganizationsDBApi { const updatePayload = {}; if (data.name !== undefined) updatePayload.name = data.name; + if (data.address !== undefined) updatePayload.address = data.address; + if (data.city !== undefined) updatePayload.city = data.city; + if (data.state !== undefined) updatePayload.state = data.state; + if (data.country !== undefined) updatePayload.country = data.country; + if (data.zip !== undefined) updatePayload.zip = data.zip; + if (data.navOrientation !== undefined) updatePayload.navOrientation = data.navOrientation; updatePayload.updatedById = currentUser.id; @@ -153,8 +170,7 @@ module.exports = class OrganizationsDBApi { const transaction = (options && options.transaction) || undefined; const organizations = await db.organizations.findOne( - { where }, - { transaction }, + { where, transaction }, ); if (!organizations) { @@ -408,4 +424,3 @@ module.exports = class OrganizationsDBApi { }; - diff --git a/backend/src/db/api/projects.js b/backend/src/db/api/projects.js index 7003cc9..4412d03 100644 --- a/backend/src/db/api/projects.js +++ b/backend/src/db/api/projects.js @@ -1,4 +1,3 @@ - const db = require('../models'); const FileDBApi = require('./file'); const crypto = require('crypto'); @@ -149,9 +148,6 @@ module.exports = class ProjectsDBApi { data[i].attachments, options, ); - } - - for (let i = 0; i < projects.length; i++) { await FileDBApi.replaceRelationFiles( { belongsTo: db.projects.getTableName(), @@ -324,22 +320,12 @@ module.exports = class ProjectsDBApi { - - - output.trials_project = await projects.getTrials_project({ transaction }); - - - - - - - output.tenant = await projects.getTenant({ transaction }); @@ -350,6 +336,11 @@ module.exports = class ProjectsDBApi { }); + output.organizations = await projects.getOrganizations({ + transaction + }); + + output.attachments = await projects.getAttachments({ transaction }); @@ -389,6 +380,10 @@ module.exports = class ProjectsDBApi { where.organizationsId = options.currentUser.organizationsId; } } + + if (!globalAccess && options?.currentUser?.tenantId) { + where.tenantId = options.currentUser.tenantId; + } offset = currentPage * limit; @@ -556,10 +551,30 @@ module.exports = class ProjectsDBApi { + if (filter.tenant) { + const listItems = filter.tenant.split('|').map(item => { + return Utils.uuid(item) + }); + where = { + ...where, + tenantId: {[Op.or]: listItems} + }; + } + if (filter.location) { + const listItems = filter.location.split('|').map(item => { + return Utils.uuid(item) + }); + + where = { + ...where, + locationId: {[Op.or]: listItems} + }; + } + if (filter.organizations) { const listItems = filter.organizations.split('|').map(item => { return Utils.uuid(item) @@ -672,5 +687,4 @@ module.exports = class ProjectsDBApi { } -}; - +}; \ No newline at end of file diff --git a/backend/src/db/api/reports.js b/backend/src/db/api/reports.js index 5add70d..f1d5d49 100644 --- a/backend/src/db/api/reports.js +++ b/backend/src/db/api/reports.js @@ -335,6 +335,10 @@ module.exports = class ReportsDBApi { where.organizationsId = options.currentUser.organizationsId; } } + + if (!globalAccess && options?.currentUser?.tenantId) { + where.tenantId = options.currentUser.tenantId; + } offset = currentPage * limit; diff --git a/backend/src/db/api/submission_values.js b/backend/src/db/api/submission_values.js index 273eaf9..850f118 100644 --- a/backend/src/db/api/submission_values.js +++ b/backend/src/db/api/submission_values.js @@ -405,6 +405,10 @@ module.exports = class Submission_valuesDBApi { where.organizationsId = options.currentUser.organizationsId; } } + + if (!globalAccess && options?.currentUser?.tenantId) { + where.tenantId = options.currentUser.tenantId; + } offset = currentPage * limit; diff --git a/backend/src/db/api/trial_types.js b/backend/src/db/api/trial_types.js index 2ae4fb1..214f66e 100644 --- a/backend/src/db/api/trial_types.js +++ b/backend/src/db/api/trial_types.js @@ -284,6 +284,10 @@ module.exports = class Trial_typesDBApi { where.organizationsId = options.currentUser.organizationsId; } } + + if (!globalAccess && options?.currentUser?.tenantId) { + where.tenantId = options.currentUser.tenantId; + } offset = currentPage * limit; diff --git a/backend/src/db/api/trials.js b/backend/src/db/api/trials.js index 70b75c1..6a3be2e 100644 --- a/backend/src/db/api/trials.js +++ b/backend/src/db/api/trials.js @@ -434,6 +434,10 @@ module.exports = class TrialsDBApi { where.organizationsId = options.currentUser.organizationsId; } } + + if (!globalAccess && options?.currentUser?.tenantId) { + where.tenantId = options.currentUser.tenantId; + } offset = currentPage * limit; diff --git a/backend/src/db/api/users.js b/backend/src/db/api/users.js index 0ab5122..2d53808 100644 --- a/backend/src/db/api/users.js +++ b/backend/src/db/api/users.js @@ -1,4 +1,3 @@ - const db = require('../models'); const FileDBApi = require('./file'); const crypto = require('crypto'); @@ -116,7 +115,11 @@ module.exports = class UsersDBApi { transaction, }); - + if (data.data.tenant !== undefined) { + await users.setTenant(data.data.tenant || null, { + transaction, + }); + } await users.setCustom_permissions(data.data.custom_permissions || [], { @@ -329,6 +332,12 @@ module.exports = class UsersDBApi { { transaction } ); } + + if (data.tenant !== undefined) { + await users.setTenant(data.tenant || null, { + transaction, + }); + } @@ -404,73 +413,41 @@ module.exports = class UsersDBApi { static async findBy(where, options) { const transaction = (options && options.transaction) || undefined; - const users = await db.users.findOne( - { where }, - { transaction }, - ); + const user = await db.users.findOne({ + where, + transaction, + include: [ + { model: db.roles, as: "app_role" }, + { model: db.organizations, as: "organizations" }, + { model: db.tenants, as: "tenant" }, + { model: db.file, as: "avatar" }, + { model: db.permissions, as: "custom_permissions" } + ] + }); - if (!users) { - return users; + if (!user) { + return user; } - const output = users.get({plain: true}); + const output = user.get({ plain: true }); - - - - - - - - - - - - - - output.form_submissions_submitted_by_user = await users.getForm_submissions_submitted_by_user({ - transaction - }); - - - - output.activity_feed_items_actor_user = await users.getActivity_feed_items_actor_user({ - transaction - }); - - - output.reports_created_by_user = await users.getReports_created_by_user({ - transaction - }); - - - - output.avatar = await users.getAvatar({ - transaction - }); - - - output.app_role = await users.getApp_role({ - transaction - }); - - if (output.app_role) { - output.app_role_permissions = await output.app_role.getPermissions({ + if (user.app_role) { + output.app_role_permissions = await user.app_role.getPermissions({ transaction, }); } - - - output.custom_permissions = await users.getCustom_permissions({ + + output.form_submissions_submitted_by_user = await user.getForm_submissions_submitted_by_user({ transaction }); - - - output.organizations = await users.getOrganizations({ + + output.activity_feed_items_actor_user = await user.getActivity_feed_items_actor_user({ + transaction + }); + + output.reports_created_by_user = await user.getReports_created_by_user({ transaction }); - - return output; } @@ -528,6 +505,11 @@ module.exports = class UsersDBApi { }, + { + model: db.tenants, + as: 'tenant', + }, + { model: db.permissions, @@ -1007,5 +989,4 @@ module.exports = class UsersDBApi { -}; - +}; \ No newline at end of file diff --git a/backend/src/db/migrations/1771796084954.js b/backend/src/db/migrations/1771796084954.js new file mode 100644 index 0000000..5734512 --- /dev/null +++ b/backend/src/db/migrations/1771796084954.js @@ -0,0 +1,83 @@ + +module.exports = { + async up(queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'organizations', + 'address', + { + type: Sequelize.DataTypes.TEXT, + allowNull: true, + }, + { transaction } + ); + await queryInterface.addColumn( + 'organizations', + 'city', + { + type: Sequelize.DataTypes.TEXT, + allowNull: true, + }, + { transaction } + ); + await queryInterface.addColumn( + 'organizations', + 'state', + { + type: Sequelize.DataTypes.TEXT, + allowNull: true, + }, + { transaction } + ); + await queryInterface.addColumn( + 'organizations', + 'country', + { + type: Sequelize.DataTypes.TEXT, + allowNull: true, + }, + { transaction } + ); + await queryInterface.addColumn( + 'organizations', + 'zip', + { + type: Sequelize.DataTypes.TEXT, + allowNull: true, + }, + { transaction } + ); + await queryInterface.addColumn( + 'organizations', + 'navOrientation', + { + type: Sequelize.DataTypes.TEXT, + allowNull: false, + defaultValue: 'side', + }, + { transaction } + ); + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + + async down(queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('organizations', 'address', { transaction }); + await queryInterface.removeColumn('organizations', 'city', { transaction }); + await queryInterface.removeColumn('organizations', 'state', { transaction }); + await queryInterface.removeColumn('organizations', 'country', { transaction }); + await queryInterface.removeColumn('organizations', 'zip', { transaction }); + await queryInterface.removeColumn('organizations', 'navOrientation', { transaction }); + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/1771796084955.js b/backend/src/db/migrations/1771796084955.js new file mode 100644 index 0000000..cdc830a --- /dev/null +++ b/backend/src/db/migrations/1771796084955.js @@ -0,0 +1,56 @@ +module.exports = { + async up(queryInterface, Sequelize) { + const createdAt = new Date(); + const updatedAt = new Date(); + + const roleIds = [ + '21a13835-6fd7-4672-b383-c609e9d5da87', // Administrator + '7fd7feec-5dd7-490a-bedc-361ebe87415e', // Tenant Owner + '61b8b77a-5114-458a-bb16-fd554d4f4557', // Global Operations Lead + '89ecce95-8ac5-4ebb-bdb2-7746fe7e4690', // Research Manager + ]; + + const permissionIds = [ + '80485725-0adf-4309-a2ee-177870dcc3ee', // READ_ORGANIZATIONS + 'a3d8dfaf-6c52-42d5-a2f3-e7e19c593e3d', // UPDATE_ORGANIZATIONS + ]; + + const rolesPermissions = []; + + for (const roleId of roleIds) { + for (const permissionId of permissionIds) { + rolesPermissions.push({ + createdAt, + updatedAt, + roles_permissionsId: roleId, + permissionId: permissionId, + }); + } + } + + // Use a transaction and UPSERT-like behavior or just check existence + // To keep it simple, we use queryInterface.bulkInsert but it might fail on duplicates if we are not careful. + // However, we checked and they are missing. + + await queryInterface.bulkInsert('rolesPermissionsPermissions', rolesPermissions); + }, + + async down(queryInterface, Sequelize) { + const roleIds = [ + '21a13835-6fd7-4672-b383-c609e9d5da87', // Administrator + '7fd7feec-5dd7-490a-bedc-361ebe87415e', // Tenant Owner + '61b8b77a-5114-458a-bb16-fd554d4f4557', // Global Operations Lead + '89ecce95-8ac5-4ebb-bdb2-7746fe7e4690', // Research Manager + ]; + + const permissionIds = [ + '80485725-0adf-4309-a2ee-177870dcc3ee', // READ_ORGANIZATIONS + 'a3d8dfaf-6c52-42d5-a2f3-e7e19c593e3d', // UPDATE_ORGANIZATIONS + ]; + + await queryInterface.bulkDelete('rolesPermissionsPermissions', { + roles_permissionsId: roleIds, + permissionId: permissionIds, + }); + } +}; \ No newline at end of file diff --git a/backend/src/db/migrations/1771796084956.js b/backend/src/db/migrations/1771796084956.js new file mode 100644 index 0000000..25d40a6 --- /dev/null +++ b/backend/src/db/migrations/1771796084956.js @@ -0,0 +1,46 @@ +module.exports = { + async up(queryInterface, Sequelize) { + const roles = await queryInterface.sequelize.query( + `SELECT id, name FROM roles WHERE name IN ('Administrator', 'Tenant Owner', 'Global Operations Lead', 'Research Manager')`, + { type: Sequelize.QueryTypes.SELECT } + ); + + const permissions = await queryInterface.sequelize.query( + `SELECT id, name FROM permissions WHERE name IN ('READ_ORGANIZATIONS', 'UPDATE_ORGANIZATIONS')`, + { type: Sequelize.QueryTypes.SELECT } + ); + + const createdAt = new Date(); + const updatedAt = new Date(); + + const rolesPermissions = []; + + for (const role of roles) { + for (const permission of permissions) { + rolesPermissions.push({ + createdAt, + updatedAt, + roles_permissionsId: role.id, + permissionId: permission.id, + }); + } + } + + // Use a more robust way to insert ignoring duplicates + for (const rp of rolesPermissions) { + await queryInterface.sequelize.query( + `INSERT INTO "rolesPermissionsPermissions" ("createdAt", "updatedAt", "roles_permissionsId", "permissionId") + VALUES (?, ?, ?, ?) + ON CONFLICT ("roles_permissionsId", "permissionId") DO NOTHING`, + { + replacements: [rp.createdAt, rp.updatedAt, rp.roles_permissionsId, rp.permissionId], + type: Sequelize.QueryTypes.INSERT + } + ); + } + }, + + async down(queryInterface, Sequelize) { + // No need for a down migration that removes permissions as it might remove more than intended if not careful + } +}; diff --git a/backend/src/db/migrations/20260222000000-add-tenant-to-users.js b/backend/src/db/migrations/20260222000000-add-tenant-to-users.js new file mode 100644 index 0000000..dc89531 --- /dev/null +++ b/backend/src/db/migrations/20260222000000-add-tenant-to-users.js @@ -0,0 +1,18 @@ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.addColumn('users', 'tenantId', { + type: Sequelize.DataTypes.UUID, + references: { + model: 'tenants', + key: 'id', + }, + allowNull: true, + onUpdate: 'CASCADE', + onDelete: 'SET NULL', + }); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.removeColumn('users', 'tenantId'); + }, +}; diff --git a/backend/src/db/migrations/20260222000001-link-admin-to-tenant.js b/backend/src/db/migrations/20260222000001-link-admin-to-tenant.js new file mode 100644 index 0000000..7686265 --- /dev/null +++ b/backend/src/db/migrations/20260222000001-link-admin-to-tenant.js @@ -0,0 +1,13 @@ +module.exports = { + async up(queryInterface, Sequelize) { + const [tenants] = await queryInterface.sequelize.query("SELECT id FROM tenants WHERE name = 'Green Valley Farms' LIMIT 1;"); + if (tenants && tenants.length > 0) { + const tenantId = tenants[0].id; + await queryInterface.sequelize.query("UPDATE users SET \"tenantId\" = '" + tenantId + "' WHERE email = 'admin@flatlogic.com';"); + } + }, + + async down(queryInterface, Sequelize) { + await queryInterface.sequelize.query("UPDATE users SET \"tenantId\" = NULL WHERE email = 'admin@flatlogic.com';"); + }, +}; diff --git a/backend/src/db/models/organizations.js b/backend/src/db/models/organizations.js index 062b9d3..15247f6 100644 --- a/backend/src/db/models/organizations.js +++ b/backend/src/db/models/organizations.js @@ -14,11 +14,34 @@ module.exports = function(sequelize, DataTypes) { primaryKey: true, }, -name: { + name: { type: DataTypes.TEXT, - - + }, + address: { + type: DataTypes.TEXT, + }, + + city: { + type: DataTypes.TEXT, + }, + + state: { + type: DataTypes.TEXT, + }, + + country: { + type: DataTypes.TEXT, + }, + + zip: { + type: DataTypes.TEXT, + }, + + navOrientation: { + type: DataTypes.TEXT, + allowNull: false, + defaultValue: 'side', }, importHash: { @@ -180,6 +203,4 @@ name: { return organizations; -}; - - +}; \ No newline at end of file diff --git a/backend/src/db/models/users.js b/backend/src/db/models/users.js index ce0aea9..e5e55d4 100644 --- a/backend/src/db/models/users.js +++ b/backend/src/db/models/users.js @@ -13,97 +13,46 @@ module.exports = function(sequelize, DataTypes) { defaultValue: DataTypes.UUIDV4, primaryKey: true, }, - -firstName: { + firstName: { type: DataTypes.TEXT, - - - }, - -lastName: { + lastName: { type: DataTypes.TEXT, - - - }, - -phoneNumber: { + phoneNumber: { type: DataTypes.TEXT, - - - }, - -email: { + email: { type: DataTypes.TEXT, - - - }, - -disabled: { + disabled: { type: DataTypes.BOOLEAN, - allowNull: false, defaultValue: false, - - - }, - -password: { + password: { type: DataTypes.TEXT, - - - }, - -emailVerified: { + emailVerified: { type: DataTypes.BOOLEAN, - allowNull: false, defaultValue: false, - - - }, - -emailVerificationToken: { + emailVerificationToken: { type: DataTypes.TEXT, - - - }, - -emailVerificationTokenExpiresAt: { + emailVerificationTokenExpiresAt: { type: DataTypes.DATE, - - - }, - -passwordResetToken: { + passwordResetToken: { type: DataTypes.TEXT, - - - }, - -passwordResetTokenExpiresAt: { + passwordResetTokenExpiresAt: { type: DataTypes.DATE, - - - }, - -provider: { + provider: { type: DataTypes.TEXT, - - - }, - importHash: { type: DataTypes.STRING(255), allowNull: true, @@ -118,7 +67,6 @@ provider: { ); users.associate = (db) => { - db.users.belongsToMany(db.permissions, { as: 'custom_permissions', foreignKey: { @@ -137,22 +85,6 @@ provider: { through: 'usersCustom_permissionsPermissions', }); - -/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - - - - - - - - - - - - - - db.users.hasMany(db.form_submissions, { as: 'form_submissions_submitted_by_user', foreignKey: { @@ -161,8 +93,6 @@ provider: { constraints: false, }); - - db.users.hasMany(db.activity_feed_items, { as: 'activity_feed_items_actor_user', foreignKey: { @@ -171,7 +101,6 @@ provider: { constraints: false, }); - db.users.hasMany(db.reports, { as: 'reports_created_by_user', foreignKey: { @@ -180,12 +109,6 @@ provider: { constraints: false, }); - - -//end loop - - - db.users.belongsTo(db.roles, { as: 'app_role', foreignKey: { @@ -202,7 +125,13 @@ provider: { constraints: false, }); - + db.users.belongsTo(db.tenants, { + as: 'tenant', + foreignKey: { + name: 'tenantId', + }, + constraints: false, + }); db.users.hasMany(db.file, { as: 'avatar', @@ -214,7 +143,6 @@ provider: { }, }); - db.users.belongsTo(db.users, { as: 'createdBy', }); @@ -224,48 +152,31 @@ provider: { }); }; - - users.beforeCreate((users, options) => { - users = trimStringFields(users); + users.beforeCreate((users, options) => { + users = trimStringFields(users); if (users.provider !== providers.LOCAL && Object.values(providers).indexOf(users.provider) > -1) { - users.emailVerified = true; + users.emailVerified = true; - if (!users.password) { - const password = crypto - .randomBytes(20) - .toString('hex'); - - const hashedPassword = bcrypt.hashSync( - password, - config.bcrypt.saltRounds, - ); - - users.password = hashedPassword - } - } - }); + if (!users.password) { + const password = crypto.randomBytes(20).toString('hex'); + const saltRounds = (config.bcrypt && config.bcrypt.saltRounds) || 10; + const hashedPassword = bcrypt.hashSync(password, saltRounds); + users.password = hashedPassword; + } + } + }); users.beforeUpdate((users, options) => { users = trimStringFields(users); }); - return users; }; - function trimStringFields(users) { - users.email = users.email.trim(); - - users.firstName = users.firstName - ? users.firstName.trim() - : null; - - users.lastName = users.lastName - ? users.lastName.trim() - : null; - + users.email = users.email ? users.email.trim() : ''; + users.firstName = users.firstName ? users.firstName.trim() : null; + users.lastName = users.lastName ? users.lastName.trim() : null; return users; -} - +} \ No newline at end of file diff --git a/backend/src/db/seeders/20231127130745-sample-data.js b/backend/src/db/seeders/20231127130745-sample-data.js index dde1c4c..765728b 100644 --- a/backend/src/db/seeders/20231127130745-sample-data.js +++ b/backend/src/db/seeders/20231127130745-sample-data.js @@ -3529,11 +3529,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 0 }); - if (User0?.setOrganization) + if (User0?.setOrganizations) { await User0. - setOrganization(relatedOrganization0); + setOrganizations(relatedOrganization0); } const relatedOrganization1 = await Organizations.findOne({ @@ -3543,11 +3543,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 1 }); - if (User1?.setOrganization) + if (User1?.setOrganizations) { await User1. - setOrganization(relatedOrganization1); + setOrganizations(relatedOrganization1); } const relatedOrganization2 = await Organizations.findOne({ @@ -3557,11 +3557,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 2 }); - if (User2?.setOrganization) + if (User2?.setOrganizations) { await User2. - setOrganization(relatedOrganization2); + setOrganizations(relatedOrganization2); } const relatedOrganization3 = await Organizations.findOne({ @@ -3571,11 +3571,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 3 }); - if (User3?.setOrganization) + if (User3?.setOrganizations) { await User3. - setOrganization(relatedOrganization3); + setOrganizations(relatedOrganization3); } } @@ -3616,11 +3616,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 0 }); - if (Tenant0?.setOrganization) + if (Tenant0?.setOrganizations) { await Tenant0. - setOrganization(relatedOrganization0); + setOrganizations(relatedOrganization0); } const relatedOrganization1 = await Organizations.findOne({ @@ -3630,11 +3630,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 1 }); - if (Tenant1?.setOrganization) + if (Tenant1?.setOrganizations) { await Tenant1. - setOrganization(relatedOrganization1); + setOrganizations(relatedOrganization1); } const relatedOrganization2 = await Organizations.findOne({ @@ -3644,11 +3644,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 2 }); - if (Tenant2?.setOrganization) + if (Tenant2?.setOrganizations) { await Tenant2. - setOrganization(relatedOrganization2); + setOrganizations(relatedOrganization2); } const relatedOrganization3 = await Organizations.findOne({ @@ -3658,11 +3658,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 3 }); - if (Tenant3?.setOrganization) + if (Tenant3?.setOrganizations) { await Tenant3. - setOrganization(relatedOrganization3); + setOrganizations(relatedOrganization3); } } @@ -3823,11 +3823,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 0 }); - if (Location0?.setOrganization) + if (Location0?.setOrganizations) { await Location0. - setOrganization(relatedOrganization0); + setOrganizations(relatedOrganization0); } const relatedOrganization1 = await Organizations.findOne({ @@ -3837,11 +3837,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 1 }); - if (Location1?.setOrganization) + if (Location1?.setOrganizations) { await Location1. - setOrganization(relatedOrganization1); + setOrganizations(relatedOrganization1); } const relatedOrganization2 = await Organizations.findOne({ @@ -3851,11 +3851,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 2 }); - if (Location2?.setOrganization) + if (Location2?.setOrganizations) { await Location2. - setOrganization(relatedOrganization2); + setOrganizations(relatedOrganization2); } const relatedOrganization3 = await Organizations.findOne({ @@ -3865,11 +3865,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 3 }); - if (Location3?.setOrganization) + if (Location3?.setOrganizations) { await Location3. - setOrganization(relatedOrganization3); + setOrganizations(relatedOrganization3); } } @@ -4030,11 +4030,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 0 }); - if (Project0?.setOrganization) + if (Project0?.setOrganizations) { await Project0. - setOrganization(relatedOrganization0); + setOrganizations(relatedOrganization0); } const relatedOrganization1 = await Organizations.findOne({ @@ -4044,11 +4044,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 1 }); - if (Project1?.setOrganization) + if (Project1?.setOrganizations) { await Project1. - setOrganization(relatedOrganization1); + setOrganizations(relatedOrganization1); } const relatedOrganization2 = await Organizations.findOne({ @@ -4058,11 +4058,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 2 }); - if (Project2?.setOrganization) + if (Project2?.setOrganizations) { await Project2. - setOrganization(relatedOrganization2); + setOrganizations(relatedOrganization2); } const relatedOrganization3 = await Organizations.findOne({ @@ -4072,11 +4072,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 3 }); - if (Project3?.setOrganization) + if (Project3?.setOrganizations) { await Project3. - setOrganization(relatedOrganization3); + setOrganizations(relatedOrganization3); } } @@ -4168,11 +4168,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 0 }); - if (TrialType0?.setOrganization) + if (TrialType0?.setOrganizations) { await TrialType0. - setOrganization(relatedOrganization0); + setOrganizations(relatedOrganization0); } const relatedOrganization1 = await Organizations.findOne({ @@ -4182,11 +4182,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 1 }); - if (TrialType1?.setOrganization) + if (TrialType1?.setOrganizations) { await TrialType1. - setOrganization(relatedOrganization1); + setOrganizations(relatedOrganization1); } const relatedOrganization2 = await Organizations.findOne({ @@ -4196,11 +4196,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 2 }); - if (TrialType2?.setOrganization) + if (TrialType2?.setOrganizations) { await TrialType2. - setOrganization(relatedOrganization2); + setOrganizations(relatedOrganization2); } const relatedOrganization3 = await Organizations.findOne({ @@ -4210,11 +4210,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 3 }); - if (TrialType3?.setOrganization) + if (TrialType3?.setOrganizations) { await TrialType3. - setOrganization(relatedOrganization3); + setOrganizations(relatedOrganization3); } } @@ -4503,11 +4503,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 0 }); - if (Trial0?.setOrganization) + if (Trial0?.setOrganizations) { await Trial0. - setOrganization(relatedOrganization0); + setOrganizations(relatedOrganization0); } const relatedOrganization1 = await Organizations.findOne({ @@ -4517,11 +4517,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 1 }); - if (Trial1?.setOrganization) + if (Trial1?.setOrganizations) { await Trial1. - setOrganization(relatedOrganization1); + setOrganizations(relatedOrganization1); } const relatedOrganization2 = await Organizations.findOne({ @@ -4531,11 +4531,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 2 }); - if (Trial2?.setOrganization) + if (Trial2?.setOrganizations) { await Trial2. - setOrganization(relatedOrganization2); + setOrganizations(relatedOrganization2); } const relatedOrganization3 = await Organizations.findOne({ @@ -4545,11 +4545,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 3 }); - if (Trial3?.setOrganization) + if (Trial3?.setOrganizations) { await Trial3. - setOrganization(relatedOrganization3); + setOrganizations(relatedOrganization3); } } @@ -4647,11 +4647,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 0 }); - if (FormTemplate0?.setOrganization) + if (FormTemplate0?.setOrganizations) { await FormTemplate0. - setOrganization(relatedOrganization0); + setOrganizations(relatedOrganization0); } const relatedOrganization1 = await Organizations.findOne({ @@ -4661,11 +4661,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 1 }); - if (FormTemplate1?.setOrganization) + if (FormTemplate1?.setOrganizations) { await FormTemplate1. - setOrganization(relatedOrganization1); + setOrganizations(relatedOrganization1); } const relatedOrganization2 = await Organizations.findOne({ @@ -4675,11 +4675,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 2 }); - if (FormTemplate2?.setOrganization) + if (FormTemplate2?.setOrganizations) { await FormTemplate2. - setOrganization(relatedOrganization2); + setOrganizations(relatedOrganization2); } const relatedOrganization3 = await Organizations.findOne({ @@ -4689,11 +4689,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 3 }); - if (FormTemplate3?.setOrganization) + if (FormTemplate3?.setOrganizations) { await FormTemplate3. - setOrganization(relatedOrganization3); + setOrganizations(relatedOrganization3); } } @@ -4856,11 +4856,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 0 }); - if (FormField0?.setOrganization) + if (FormField0?.setOrganizations) { await FormField0. - setOrganization(relatedOrganization0); + setOrganizations(relatedOrganization0); } const relatedOrganization1 = await Organizations.findOne({ @@ -4870,11 +4870,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 1 }); - if (FormField1?.setOrganization) + if (FormField1?.setOrganizations) { await FormField1. - setOrganization(relatedOrganization1); + setOrganizations(relatedOrganization1); } const relatedOrganization2 = await Organizations.findOne({ @@ -4884,11 +4884,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 2 }); - if (FormField2?.setOrganization) + if (FormField2?.setOrganizations) { await FormField2. - setOrganization(relatedOrganization2); + setOrganizations(relatedOrganization2); } const relatedOrganization3 = await Organizations.findOne({ @@ -4898,11 +4898,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 3 }); - if (FormField3?.setOrganization) + if (FormField3?.setOrganizations) { await FormField3. - setOrganization(relatedOrganization3); + setOrganizations(relatedOrganization3); } } @@ -5057,11 +5057,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 0 }); - if (FormFieldChoice0?.setOrganization) + if (FormFieldChoice0?.setOrganizations) { await FormFieldChoice0. - setOrganization(relatedOrganization0); + setOrganizations(relatedOrganization0); } const relatedOrganization1 = await Organizations.findOne({ @@ -5071,11 +5071,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 1 }); - if (FormFieldChoice1?.setOrganization) + if (FormFieldChoice1?.setOrganizations) { await FormFieldChoice1. - setOrganization(relatedOrganization1); + setOrganizations(relatedOrganization1); } const relatedOrganization2 = await Organizations.findOne({ @@ -5085,11 +5085,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 2 }); - if (FormFieldChoice2?.setOrganization) + if (FormFieldChoice2?.setOrganizations) { await FormFieldChoice2. - setOrganization(relatedOrganization2); + setOrganizations(relatedOrganization2); } const relatedOrganization3 = await Organizations.findOne({ @@ -5099,11 +5099,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 3 }); - if (FormFieldChoice3?.setOrganization) + if (FormFieldChoice3?.setOrganizations) { await FormFieldChoice3. - setOrganization(relatedOrganization3); + setOrganizations(relatedOrganization3); } } @@ -5390,11 +5390,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 0 }); - if (FormSubmission0?.setOrganization) + if (FormSubmission0?.setOrganizations) { await FormSubmission0. - setOrganization(relatedOrganization0); + setOrganizations(relatedOrganization0); } const relatedOrganization1 = await Organizations.findOne({ @@ -5404,11 +5404,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 1 }); - if (FormSubmission1?.setOrganization) + if (FormSubmission1?.setOrganizations) { await FormSubmission1. - setOrganization(relatedOrganization1); + setOrganizations(relatedOrganization1); } const relatedOrganization2 = await Organizations.findOne({ @@ -5418,11 +5418,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 2 }); - if (FormSubmission2?.setOrganization) + if (FormSubmission2?.setOrganizations) { await FormSubmission2. - setOrganization(relatedOrganization2); + setOrganizations(relatedOrganization2); } const relatedOrganization3 = await Organizations.findOne({ @@ -5432,11 +5432,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 3 }); - if (FormSubmission3?.setOrganization) + if (FormSubmission3?.setOrganizations) { await FormSubmission3. - setOrganization(relatedOrganization3); + setOrganizations(relatedOrganization3); } } @@ -5660,11 +5660,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 0 }); - if (SubmissionValue0?.setOrganization) + if (SubmissionValue0?.setOrganizations) { await SubmissionValue0. - setOrganization(relatedOrganization0); + setOrganizations(relatedOrganization0); } const relatedOrganization1 = await Organizations.findOne({ @@ -5674,11 +5674,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 1 }); - if (SubmissionValue1?.setOrganization) + if (SubmissionValue1?.setOrganizations) { await SubmissionValue1. - setOrganization(relatedOrganization1); + setOrganizations(relatedOrganization1); } const relatedOrganization2 = await Organizations.findOne({ @@ -5688,11 +5688,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 2 }); - if (SubmissionValue2?.setOrganization) + if (SubmissionValue2?.setOrganizations) { await SubmissionValue2. - setOrganization(relatedOrganization2); + setOrganizations(relatedOrganization2); } const relatedOrganization3 = await Organizations.findOne({ @@ -5702,11 +5702,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 3 }); - if (SubmissionValue3?.setOrganization) + if (SubmissionValue3?.setOrganizations) { await SubmissionValue3. - setOrganization(relatedOrganization3); + setOrganizations(relatedOrganization3); } } @@ -5865,11 +5865,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 0 }); - if (ActivityFeedItem0?.setOrganization) + if (ActivityFeedItem0?.setOrganizations) { await ActivityFeedItem0. - setOrganization(relatedOrganization0); + setOrganizations(relatedOrganization0); } const relatedOrganization1 = await Organizations.findOne({ @@ -5879,11 +5879,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 1 }); - if (ActivityFeedItem1?.setOrganization) + if (ActivityFeedItem1?.setOrganizations) { await ActivityFeedItem1. - setOrganization(relatedOrganization1); + setOrganizations(relatedOrganization1); } const relatedOrganization2 = await Organizations.findOne({ @@ -5893,11 +5893,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 2 }); - if (ActivityFeedItem2?.setOrganization) + if (ActivityFeedItem2?.setOrganizations) { await ActivityFeedItem2. - setOrganization(relatedOrganization2); + setOrganizations(relatedOrganization2); } const relatedOrganization3 = await Organizations.findOne({ @@ -5907,11 +5907,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 3 }); - if (ActivityFeedItem3?.setOrganization) + if (ActivityFeedItem3?.setOrganizations) { await ActivityFeedItem3. - setOrganization(relatedOrganization3); + setOrganizations(relatedOrganization3); } } @@ -6068,11 +6068,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 0 }); - if (Report0?.setOrganization) + if (Report0?.setOrganizations) { await Report0. - setOrganization(relatedOrganization0); + setOrganizations(relatedOrganization0); } const relatedOrganization1 = await Organizations.findOne({ @@ -6082,11 +6082,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 1 }); - if (Report1?.setOrganization) + if (Report1?.setOrganizations) { await Report1. - setOrganization(relatedOrganization1); + setOrganizations(relatedOrganization1); } const relatedOrganization2 = await Organizations.findOne({ @@ -6096,11 +6096,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 2 }); - if (Report2?.setOrganization) + if (Report2?.setOrganizations) { await Report2. - setOrganization(relatedOrganization2); + setOrganizations(relatedOrganization2); } const relatedOrganization3 = await Organizations.findOne({ @@ -6110,11 +6110,11 @@ const ReportsData = [ order: [['id', 'ASC']], offset: 3 }); - if (Report3?.setOrganization) + if (Report3?.setOrganizations) { await Report3. - setOrganization(relatedOrganization3); + setOrganizations(relatedOrganization3); } } diff --git a/backend/src/middlewares/check-permissions.js b/backend/src/middlewares/check-permissions.js index 77740c7..be92e24 100644 --- a/backend/src/middlewares/check-permissions.js +++ b/backend/src/middlewares/check-permissions.js @@ -1,4 +1,3 @@ - const ValidationError = require('../services/notifications/errors/validation'); const RolesDBApi = require('../db/api/roles'); @@ -49,6 +48,11 @@ function checkPermissions(permission) { // 2. Check Custom Permissions (only if the user is authenticated) if (currentUser) { + // Check for Super Admin (globalAccess) + if (currentUser.app_role && currentUser.app_role.globalAccess) { + return next(); + } + // Ensure custom_permissions is an array before using find const customPermissions = Array.isArray(currentUser.custom_permissions) ? currentUser.custom_permissions @@ -91,10 +95,12 @@ function checkPermissions(permission) { } // 4. Check Permissions on the "effective" role - // Assume the effectiveRole object (from app_role or RolesDBApi) has a getPermissions() method - // or a 'permissions' property (if permissions are eagerly loaded). let rolePermissions = []; - if (typeof effectiveRole.getPermissions === 'function') { + + // Check if permissions are already available in currentUser (from UsersDBApi.findBy) + if (currentUser && currentUser.app_role_permissions) { + rolePermissions = currentUser.app_role_permissions; + } else if (typeof effectiveRole.getPermissions === 'function') { rolePermissions = await effectiveRole.getPermissions(); // Get permissions asynchronously if the method exists } else if (Array.isArray(effectiveRole.permissions)) { rolePermissions = effectiveRole.permissions; // Or take from property if permissions are pre-loaded @@ -145,5 +151,4 @@ function checkCrudPermissions(name) { module.exports = { checkPermissions, checkCrudPermissions, -}; - +}; \ No newline at end of file diff --git a/backend/src/services/auth.js b/backend/src/services/auth.js index bcc3411..cd2c205 100644 --- a/backend/src/services/auth.js +++ b/backend/src/services/auth.js @@ -1,3 +1,4 @@ +const db = require('../db/models'); const UsersDBApi = require('../db/api/users'); const ValidationError = require('./notifications/errors/validation'); const ForbiddenError = require('./notifications/errors/forbidden'); diff --git a/frontend/src/components/AsideMenuLayer.tsx b/frontend/src/components/AsideMenuLayer.tsx index 8693dea..fa31230 100644 --- a/frontend/src/components/AsideMenuLayer.tsx +++ b/frontend/src/components/AsideMenuLayer.tsx @@ -3,10 +3,8 @@ import { mdiLogout, mdiClose } from '@mdi/js' import BaseIcon from './BaseIcon' import AsideMenuList from './AsideMenuList' import { MenuAsideItem } from '../interfaces' -import { useAppSelector } from '../stores/hooks' +import { useAppDispatch, useAppSelector } from '../stores/hooks' import Link from 'next/link'; - -import { useAppDispatch } from '../stores/hooks'; import { createAsyncThunk } from '@reduxjs/toolkit'; import axios from 'axios'; @@ -91,4 +89,4 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props ) -} +} \ No newline at end of file diff --git a/frontend/src/components/NavBar.tsx b/frontend/src/components/NavBar.tsx index c270ae0..625805d 100644 --- a/frontend/src/components/NavBar.tsx +++ b/frontend/src/components/NavBar.tsx @@ -6,14 +6,16 @@ import NavBarItemPlain from './NavBarItemPlain' import NavBarMenuList from './NavBarMenuList' import { MenuNavBarItem } from '../interfaces' import { useAppSelector } from '../stores/hooks'; +import NavBarItem from './NavBarItem' type Props = { menu: MenuNavBarItem[] + leftMenu?: MenuNavBarItem[] className: string children: ReactNode } -export default function NavBar({ menu, className = '', children }: Props) { +export default function NavBar({ menu, leftMenu = [], className = '', children }: Props) { const [isMenuNavBarActive, setIsMenuNavBarActive] = useState(false) const [isScrolled, setIsScrolled] = useState(false); const bgColor = useAppSelector((state) => state.style.bgLayoutColor); @@ -38,7 +40,16 @@ export default function NavBar({ menu, className = '', children }: Props) { className={`${className} top-0 inset-x-0 fixed ${bgColor} h-14 z-30 transition-position w-screen lg:w-auto dark:bg-dark-800`} >
- {`${widgetsRole?.role?.label || 'Users'}'s widgets`} -
- )} + const getItemIcon = (type: string) => { + switch (type) { + case 'project_created': return icon.mdiFolderPlusOutline; + case 'trial_created': return icon.mdiFlaskPlusOutline; + case 'submission_created': return icon.mdiClipboardCheckOutline; + case 'user_created': return icon.mdiAccountPlusOutline; + case 'attachment_added': return icon.mdiPaperclip; + default: return icon.mdiBellOutline; + } + } -Projects
+Trials
+Submissions
+Team Members
+Loading feed...
} + + {!activityLoading && activity_feed_items?.length === 0 && ( +No recent activity found.
++ {item.summary} +
+ + {item.thumbnail && item.thumbnail[0] && ( ++ Trial Tracker is running in multi-tenant mode. + Farm-level data isolation is active for your account. +
++ The ultimate tool for research trials. Manage projects, monitor trials, and capture field data in real-time with our beautiful, intuitive interface. +
+{feature.description}
++ Join hundreds of researchers who trust Trial Tracker for their daily field operations. +
+This is a React.js/Node.js app generated by the Flatlogic Web App Generator
-For guides and documentation please check - your local README.md and the Flatlogic documentation
-© 2026 {title}. All rights reserved
- - Privacy Policy - -