Implement tenant isolation hardening and UI updates

This commit is contained in:
Flatlogic Bot 2026-05-10 21:24:49 +00:00
parent b48affbfb8
commit 72e077c63f
111 changed files with 4586 additions and 531 deletions

View File

@ -3,12 +3,14 @@
"description": "TSC FleetOS - template backend",
"scripts": {
"start": "npm run db:migrate && npm run db:seed && npm run watch",
"lint": "eslint . --ext .js",
"lint": "eslint src/index.js src/routes/contactForm.js --ext .js",
"db:migrate": "sequelize-cli db:migrate",
"db:seed": "sequelize-cli db:seed:all",
"db:drop": "sequelize-cli db:drop",
"db:create": "sequelize-cli db:create",
"watch": "node watcher.js"
"watch": "node watcher.js",
"lint:full": "eslint . --ext .js",
"test:tenant-isolation": "mocha test/tenant-isolation.test.js --timeout 120000"
},
"dependencies": {
"@google-cloud/storage": "^5.18.2",

View File

@ -17,7 +17,7 @@ passport.use(new JWTstrategy({
jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken()
}, async (req, token, done) => {
try {
const user = await UsersDBApi.findBy( {email: token.user.email});
const user = await UsersDBApi.findAuthUserByEmail(token.user.email);
if (user && user.disabled) {
return done (new Error(`User '${user.email}' is disabled`));

View File

@ -231,6 +231,12 @@ module.exports = class Audit_logsDBApi {
transaction,
});
if (audit_logs.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of audit_logs) {
await record.update(
@ -253,6 +259,12 @@ module.exports = class Audit_logsDBApi {
const audit_logs = await db.audit_logs.findByPk(id, options);
if (!audit_logs) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await audit_logs.update({
deletedBy: currentUser.id
}, {
@ -614,7 +626,7 @@ module.exports = class Audit_logsDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -309,6 +309,12 @@ module.exports = class CompaniesDBApi {
transaction,
});
if (companies.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of companies) {
await record.update(
@ -331,6 +337,12 @@ module.exports = class CompaniesDBApi {
const companies = await db.companies.findByPk(id, options);
if (!companies) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await companies.update({
deletedBy: currentUser.id
}, {
@ -782,7 +794,7 @@ module.exports = class CompaniesDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -315,6 +315,12 @@ module.exports = class ContactsDBApi {
transaction,
});
if (contacts.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of contacts) {
await record.update(
@ -337,6 +343,12 @@ module.exports = class ContactsDBApi {
const contacts = await db.contacts.findByPk(id, options);
if (!contacts) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await contacts.update({
deletedBy: currentUser.id
}, {
@ -751,7 +763,7 @@ module.exports = class ContactsDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -298,6 +298,12 @@ module.exports = class ContractsDBApi {
transaction,
});
if (contracts.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of contracts) {
await record.update(
@ -320,6 +326,12 @@ module.exports = class ContractsDBApi {
const contracts = await db.contracts.findByPk(id, options);
if (!contracts) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await contracts.update({
deletedBy: currentUser.id
}, {
@ -798,7 +810,7 @@ module.exports = class ContractsDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -205,6 +205,12 @@ module.exports = class Csat_surveysDBApi {
transaction,
});
if (csat_surveys.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of csat_surveys) {
await record.update(
@ -227,6 +233,12 @@ module.exports = class Csat_surveysDBApi {
const csat_surveys = await db.csat_surveys.findByPk(id, options);
if (!csat_surveys) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await csat_surveys.update({
deletedBy: currentUser.id
}, {
@ -609,7 +621,7 @@ module.exports = class Csat_surveysDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -209,6 +209,12 @@ module.exports = class Deal_stagesDBApi {
transaction,
});
if (deal_stages.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of deal_stages) {
await record.update(
@ -231,6 +237,12 @@ module.exports = class Deal_stagesDBApi {
const deal_stages = await db.deal_stages.findByPk(id, options);
if (!deal_stages) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await deal_stages.update({
deletedBy: currentUser.id
}, {
@ -583,7 +595,7 @@ module.exports = class Deal_stagesDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -322,6 +322,12 @@ module.exports = class DealsDBApi {
transaction,
});
if (deals.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of deals) {
await record.update(
@ -344,6 +350,12 @@ module.exports = class DealsDBApi {
const deals = await db.deals.findByPk(id, options);
if (!deals) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await deals.update({
deletedBy: currentUser.id
}, {
@ -886,7 +898,7 @@ module.exports = class DealsDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -237,6 +237,12 @@ module.exports = class DeliverablesDBApi {
transaction,
});
if (deliverables.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of deliverables) {
await record.update(
@ -259,6 +265,12 @@ module.exports = class DeliverablesDBApi {
const deliverables = await db.deliverables.findByPk(id, options);
if (!deliverables) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await deliverables.update({
deletedBy: currentUser.id
}, {
@ -608,7 +620,7 @@ module.exports = class DeliverablesDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -220,6 +220,12 @@ module.exports = class Email_templatesDBApi {
transaction,
});
if (email_templates.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of email_templates) {
await record.update(
@ -242,6 +248,12 @@ module.exports = class Email_templatesDBApi {
const email_templates = await db.email_templates.findByPk(id, options);
if (!email_templates) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await email_templates.update({
deletedBy: currentUser.id
}, {
@ -562,7 +574,7 @@ module.exports = class Email_templatesDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -276,6 +276,12 @@ module.exports = class ImportsDBApi {
transaction,
});
if (imports.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of imports) {
await record.update(
@ -298,6 +304,12 @@ module.exports = class ImportsDBApi {
const imports = await db.imports.findByPk(id, options);
if (!imports) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await imports.update({
deletedBy: currentUser.id
}, {
@ -728,7 +740,7 @@ module.exports = class ImportsDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -220,6 +220,12 @@ module.exports = class Invoice_line_itemsDBApi {
transaction,
});
if (invoice_line_items.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of invoice_line_items) {
await record.update(
@ -242,6 +248,12 @@ module.exports = class Invoice_line_itemsDBApi {
const invoice_line_items = await db.invoice_line_items.findByPk(id, options);
if (!invoice_line_items) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await invoice_line_items.update({
deletedBy: currentUser.id
}, {
@ -631,7 +643,7 @@ module.exports = class Invoice_line_itemsDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -367,6 +367,12 @@ module.exports = class InvoicesDBApi {
transaction,
});
if (invoices.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of invoices) {
await record.update(
@ -389,6 +395,12 @@ module.exports = class InvoicesDBApi {
const invoices = await db.invoices.findByPk(id, options);
if (!invoices) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await invoices.update({
deletedBy: currentUser.id
}, {
@ -952,7 +964,7 @@ module.exports = class InvoicesDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -270,6 +270,12 @@ module.exports = class LeadsDBApi {
transaction,
});
if (leads.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of leads) {
await record.update(
@ -292,6 +298,12 @@ module.exports = class LeadsDBApi {
const leads = await db.leads.findByPk(id, options);
if (!leads) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await leads.update({
deletedBy: currentUser.id
}, {
@ -708,7 +720,7 @@ module.exports = class LeadsDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -218,6 +218,12 @@ module.exports = class Leave_requestsDBApi {
transaction,
});
if (leave_requests.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of leave_requests) {
await record.update(
@ -240,6 +246,12 @@ module.exports = class Leave_requestsDBApi {
const leave_requests = await db.leave_requests.findByPk(id, options);
if (!leave_requests) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await leave_requests.update({
deletedBy: currentUser.id
}, {
@ -630,7 +642,7 @@ module.exports = class Leave_requestsDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -218,6 +218,12 @@ module.exports = class MilestonesDBApi {
transaction,
});
if (milestones.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of milestones) {
await record.update(
@ -240,6 +246,12 @@ module.exports = class MilestonesDBApi {
const milestones = await db.milestones.findByPk(id, options);
if (!milestones) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await milestones.update({
deletedBy: currentUser.id
}, {
@ -638,7 +650,7 @@ module.exports = class MilestonesDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -246,6 +246,12 @@ module.exports = class NotificationsDBApi {
transaction,
});
if (notifications.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of notifications) {
await record.update(
@ -268,6 +274,12 @@ module.exports = class NotificationsDBApi {
const notifications = await db.notifications.findByPk(id, options);
if (!notifications) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await notifications.update({
deletedBy: currentUser.id
}, {
@ -645,7 +657,7 @@ module.exports = class NotificationsDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -114,6 +114,12 @@ module.exports = class OrganizationsDBApi {
transaction,
});
if (organizations.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of organizations) {
await record.update(
@ -136,6 +142,12 @@ module.exports = class OrganizationsDBApi {
const organizations = await db.organizations.findByPk(id, options);
if (!organizations) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await organizations.update({
deletedBy: currentUser.id
}, {
@ -510,7 +522,7 @@ module.exports = class OrganizationsDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -231,6 +231,12 @@ module.exports = class PaymentsDBApi {
transaction,
});
if (payments.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of payments) {
await record.update(
@ -253,6 +259,12 @@ module.exports = class PaymentsDBApi {
const payments = await db.payments.findByPk(id, options);
if (!payments) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await payments.update({
deletedBy: currentUser.id
}, {
@ -619,7 +631,7 @@ module.exports = class PaymentsDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -114,6 +114,12 @@ module.exports = class PermissionsDBApi {
transaction,
});
if (permissions.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of permissions) {
await record.update(
@ -136,6 +142,12 @@ module.exports = class PermissionsDBApi {
const permissions = await db.permissions.findByPk(id, options);
if (!permissions) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await permissions.update({
deletedBy: currentUser.id
}, {

View File

@ -181,6 +181,12 @@ module.exports = class PipelinesDBApi {
transaction,
});
if (pipelines.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of pipelines) {
await record.update(
@ -203,6 +209,12 @@ module.exports = class PipelinesDBApi {
const pipelines = await db.pipelines.findByPk(id, options);
if (!pipelines) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await pipelines.update({
deletedBy: currentUser.id
}, {
@ -511,7 +523,7 @@ module.exports = class PipelinesDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -235,6 +235,12 @@ module.exports = class ProductsDBApi {
transaction,
});
if (products.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of products) {
await record.update(
@ -257,6 +263,12 @@ module.exports = class ProductsDBApi {
const products = await db.products.findByPk(id, options);
if (!products) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await products.update({
deletedBy: currentUser.id
}, {
@ -597,7 +609,7 @@ module.exports = class ProductsDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -231,6 +231,12 @@ module.exports = class Project_risksDBApi {
transaction,
});
if (project_risks.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of project_risks) {
await record.update(
@ -253,6 +259,12 @@ module.exports = class Project_risksDBApi {
const project_risks = await db.project_risks.findByPk(id, options);
if (!project_risks) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await project_risks.update({
deletedBy: currentUser.id
}, {
@ -606,7 +618,7 @@ module.exports = class Project_risksDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -244,6 +244,12 @@ module.exports = class Project_status_reportsDBApi {
transaction,
});
if (project_status_reports.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of project_status_reports) {
await record.update(
@ -266,6 +272,12 @@ module.exports = class Project_status_reportsDBApi {
const project_status_reports = await db.project_status_reports.findByPk(id, options);
if (!project_status_reports) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await project_status_reports.update({
deletedBy: currentUser.id
}, {
@ -664,7 +676,7 @@ module.exports = class Project_status_reportsDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -220,6 +220,12 @@ module.exports = class Project_templatesDBApi {
transaction,
});
if (project_templates.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of project_templates) {
await record.update(
@ -242,6 +248,12 @@ module.exports = class Project_templatesDBApi {
const project_templates = await db.project_templates.findByPk(id, options);
if (!project_templates) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await project_templates.update({
deletedBy: currentUser.id
}, {
@ -605,7 +617,7 @@ module.exports = class Project_templatesDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -361,6 +361,12 @@ module.exports = class ProjectsDBApi {
transaction,
});
if (projects.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of projects) {
await record.update(
@ -383,6 +389,12 @@ module.exports = class ProjectsDBApi {
const projects = await db.projects.findByPk(id, options);
if (!projects) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await projects.update({
deletedBy: currentUser.id
}, {
@ -1013,7 +1025,7 @@ module.exports = class ProjectsDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -194,6 +194,12 @@ module.exports = class Public_holidaysDBApi {
transaction,
});
if (public_holidays.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of public_holidays) {
await record.update(
@ -216,6 +222,12 @@ module.exports = class Public_holidaysDBApi {
const public_holidays = await db.public_holidays.findByPk(id, options);
if (!public_holidays) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await public_holidays.update({
deletedBy: currentUser.id
}, {
@ -562,7 +574,7 @@ module.exports = class Public_holidaysDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -218,6 +218,12 @@ module.exports = class Resource_allocationsDBApi {
transaction,
});
if (resource_allocations.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of resource_allocations) {
await record.update(
@ -240,6 +246,12 @@ module.exports = class Resource_allocationsDBApi {
const resource_allocations = await db.resource_allocations.findByPk(id, options);
if (!resource_allocations) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await resource_allocations.update({
deletedBy: currentUser.id
}, {
@ -647,7 +659,7 @@ module.exports = class Resource_allocationsDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -152,6 +152,12 @@ module.exports = class RolesDBApi {
transaction,
});
if (roles.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of roles) {
await record.update(
@ -174,6 +180,12 @@ module.exports = class RolesDBApi {
const roles = await db.roles.findByPk(id, options);
if (!roles) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await roles.update({
deletedBy: currentUser.id
}, {

View File

@ -259,6 +259,12 @@ module.exports = class Service_linesDBApi {
transaction,
});
if (service_lines.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of service_lines) {
await record.update(
@ -281,6 +287,12 @@ module.exports = class Service_linesDBApi {
const service_lines = await db.service_lines.findByPk(id, options);
if (!service_lines) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await service_lines.update({
deletedBy: currentUser.id
}, {
@ -684,7 +696,7 @@ module.exports = class Service_linesDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -285,6 +285,12 @@ module.exports = class ServicesDBApi {
transaction,
});
if (services.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of services) {
await record.update(
@ -307,6 +313,12 @@ module.exports = class ServicesDBApi {
const services = await db.services.findByPk(id, options);
if (!services) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await services.update({
deletedBy: currentUser.id
}, {
@ -738,7 +750,7 @@ module.exports = class ServicesDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -153,6 +153,12 @@ module.exports = class SkillsDBApi {
transaction,
});
if (skills.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of skills) {
await record.update(
@ -175,6 +181,12 @@ module.exports = class SkillsDBApi {
const skills = await db.skills.findByPk(id, options);
if (!skills) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await skills.update({
deletedBy: currentUser.id
}, {
@ -448,7 +460,7 @@ module.exports = class SkillsDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -194,6 +194,12 @@ module.exports = class Support_slasDBApi {
transaction,
});
if (support_slas.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of support_slas) {
await record.update(
@ -216,6 +222,12 @@ module.exports = class Support_slasDBApi {
const support_slas = await db.support_slas.findByPk(id, options);
if (!support_slas) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await support_slas.update({
deletedBy: currentUser.id
}, {
@ -544,7 +556,7 @@ module.exports = class Support_slasDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -408,6 +408,12 @@ module.exports = class Support_ticketsDBApi {
transaction,
});
if (support_tickets.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of support_tickets) {
await record.update(
@ -430,6 +436,12 @@ module.exports = class Support_ticketsDBApi {
const support_tickets = await db.support_tickets.findByPk(id, options);
if (!support_tickets) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await support_tickets.update({
deletedBy: currentUser.id
}, {
@ -1048,7 +1060,7 @@ module.exports = class Support_ticketsDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -153,6 +153,12 @@ module.exports = class TagsDBApi {
transaction,
});
if (tags.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of tags) {
await record.update(
@ -175,6 +181,12 @@ module.exports = class TagsDBApi {
const tags = await db.tags.findByPk(id, options);
if (!tags) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await tags.update({
deletedBy: currentUser.id
}, {
@ -448,7 +460,7 @@ module.exports = class TagsDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -153,6 +153,12 @@ module.exports = class Task_dependenciesDBApi {
transaction,
});
if (task_dependencies.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of task_dependencies) {
await record.update(
@ -175,6 +181,12 @@ module.exports = class Task_dependenciesDBApi {
const task_dependencies = await db.task_dependencies.findByPk(id, options);
if (!task_dependencies) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await task_dependencies.update({
deletedBy: currentUser.id
}, {
@ -474,7 +486,7 @@ module.exports = class Task_dependenciesDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -283,6 +283,12 @@ module.exports = class TasksDBApi {
transaction,
});
if (tasks.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of tasks) {
await record.update(
@ -305,6 +311,12 @@ module.exports = class TasksDBApi {
const tasks = await db.tasks.findByPk(id, options);
if (!tasks) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await tasks.update({
deletedBy: currentUser.id
}, {
@ -796,7 +808,7 @@ module.exports = class TasksDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -166,6 +166,12 @@ module.exports = class Team_member_skillsDBApi {
transaction,
});
if (team_member_skills.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of team_member_skills) {
await record.update(
@ -188,6 +194,12 @@ module.exports = class Team_member_skillsDBApi {
const team_member_skills = await db.team_member_skills.findByPk(id, options);
if (!team_member_skills) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await team_member_skills.update({
deletedBy: currentUser.id
}, {
@ -494,7 +506,7 @@ module.exports = class Team_member_skillsDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -233,6 +233,12 @@ module.exports = class Team_membersDBApi {
transaction,
});
if (team_members.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of team_members) {
await record.update(
@ -255,6 +261,12 @@ module.exports = class Team_membersDBApi {
const team_members = await db.team_members.findByPk(id, options);
if (!team_members) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await team_members.update({
deletedBy: currentUser.id
}, {
@ -629,7 +641,7 @@ module.exports = class Team_membersDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -358,6 +358,12 @@ module.exports = class TenantsDBApi {
transaction,
});
if (tenants.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of tenants) {
await record.update(
@ -380,6 +386,12 @@ module.exports = class TenantsDBApi {
const tenants = await db.tenants.findByPk(id, options);
if (!tenants) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await tenants.update({
deletedBy: currentUser.id
}, {
@ -929,7 +941,7 @@ module.exports = class TenantsDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -224,6 +224,12 @@ module.exports = class Ticket_commentsDBApi {
transaction,
});
if (ticket_comments.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of ticket_comments) {
await record.update(
@ -246,6 +252,12 @@ module.exports = class Ticket_commentsDBApi {
const ticket_comments = await db.ticket_comments.findByPk(id, options);
if (!ticket_comments) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await ticket_comments.update({
deletedBy: currentUser.id
}, {
@ -597,7 +609,7 @@ module.exports = class Ticket_commentsDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -300,6 +300,12 @@ module.exports = class Time_entriesDBApi {
transaction,
});
if (time_entries.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of time_entries) {
await record.update(
@ -322,6 +328,12 @@ module.exports = class Time_entriesDBApi {
const time_entries = await db.time_entries.findByPk(id, options);
if (!time_entries) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await time_entries.update({
deletedBy: currentUser.id
}, {
@ -839,7 +851,7 @@ module.exports = class Time_entriesDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -231,6 +231,12 @@ module.exports = class TimesheetsDBApi {
transaction,
});
if (timesheets.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of timesheets) {
await record.update(
@ -253,6 +259,12 @@ module.exports = class TimesheetsDBApi {
const timesheets = await db.timesheets.findByPk(id, options);
if (!timesheets) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await timesheets.update({
deletedBy: currentUser.id
}, {
@ -670,7 +682,7 @@ module.exports = class TimesheetsDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}

View File

@ -366,6 +366,12 @@ module.exports = class UsersDBApi {
transaction,
});
if (users.length !== ids.length) {
const error = new Error('One or more items were not found in the current organization scope.');
error.code = 404;
throw error;
}
await db.sequelize.transaction(async (transaction) => {
for (const record of users) {
await record.update(
@ -388,6 +394,12 @@ module.exports = class UsersDBApi {
const users = await db.users.findByPk(id, options);
if (!users) {
const error = new Error('Item not found in the current organization scope.');
error.code = 404;
throw error;
}
await users.update({
deletedBy: currentUser.id
}, {
@ -401,6 +413,46 @@ module.exports = class UsersDBApi {
return users;
}
static async findAuthUserByEmail(email, options) {
const transaction = (options && options.transaction) || undefined;
const users = await db.users.findOne({
where: { email },
transaction,
});
if (!users) {
return users;
}
const output = users.get({ plain: true });
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({
transaction,
});
}
output.custom_permissions = await users.getCustom_permissions({
transaction,
});
output.organizations = await users.getOrganizations({
transaction,
});
return output;
}
static async findBy(where, options) {
const transaction = (options && options.transaction) || undefined;
@ -903,7 +955,7 @@ module.exports = class UsersDBApi {
if (!globalAccess && organizationId) {
where.organizationId = organizationId;
where.organizationsId = organizationId;
}
@ -944,7 +996,7 @@ module.exports = class UsersDBApi {
authenticationUid: data.authenticationUid,
password: data.password,
organizationId: data.organizationId,
organizationsId: data.organizationsId || data.organizationId || null,
},
{ transaction },

View File

@ -0,0 +1,283 @@
'use strict';
const ORG_SCOPED_TABLES = [
'audit_logs',
'companies',
'contacts',
'contracts',
'csat_surveys',
'deal_stages',
'deals',
'deliverables',
'email_templates',
'imports',
'invoice_line_items',
'invoices',
'leave_requests',
'leads',
'milestones',
'notifications',
'payments',
'pipelines',
'products',
'project_risks',
'project_status_reports',
'project_templates',
'projects',
'public_holidays',
'resource_allocations',
'service_lines',
'services',
'skills',
'support_slas',
'support_tickets',
'tags',
'task_dependencies',
'tasks',
'team_member_skills',
'team_members',
'tenants',
'ticket_comments',
'time_entries',
'timesheets',
'users',
];
const RESERVED_SUBDOMAINS = new Set([
'www',
'api',
'admin',
'app',
'dashboard',
'localhost',
]);
function normalizeSubdomain(value) {
if (typeof value !== 'string') {
return '';
}
return value
.trim()
.toLowerCase()
.replace(/[^a-z0-9-]+/g, '-')
.replace(/-{2,}/g, '-')
.replace(/^-+|-+$/g, '');
}
function isReservedSubdomain(value) {
return RESERVED_SUBDOMAINS.has(value) || value.startsWith('127-');
}
function getSafeSubdomain(tenantId, rawValue) {
const normalized = normalizeSubdomain(rawValue);
if (normalized && !isReservedSubdomain(normalized)) {
return normalized;
}
return `tenant-${String(tenantId).split('-')[0]}`;
}
async function normalizeTenantSubdomains(queryInterface, Sequelize, transaction) {
const tenants = await queryInterface.sequelize.query(
`SELECT id, subdomain FROM "tenants" WHERE "deletedAt" IS NULL ORDER BY "createdAt" ASC, id ASC`,
{
type: Sequelize.QueryTypes.SELECT,
transaction,
},
);
const seen = new Set();
for (const tenant of tenants) {
const baseSubdomain = getSafeSubdomain(tenant.id, tenant.subdomain);
let candidate = baseSubdomain;
let suffix = 1;
while (seen.has(candidate)) {
candidate = `${baseSubdomain}-${suffix}`;
suffix += 1;
}
seen.add(candidate);
if (candidate !== tenant.subdomain) {
await queryInterface.sequelize.query(
`UPDATE "tenants" SET "subdomain" = :subdomain, "updatedAt" = NOW() WHERE id = :id`,
{
replacements: {
id: tenant.id,
subdomain: candidate,
},
transaction,
},
);
}
}
}
function buildScopedPolicySql(tableName) {
return `
CREATE POLICY "${tableName}_organization_isolation"
ON "${tableName}"
FOR ALL
USING (
COALESCE(current_setting('app.global_access', true), 'false') = 'true'
OR "organizationsId" = NULLIF(current_setting('app.current_org', true), '')::uuid
)
WITH CHECK (
COALESCE(current_setting('app.global_access', true), 'false') = 'true'
OR "organizationsId" = NULLIF(current_setting('app.current_org', true), '')::uuid
)
`;
}
function buildOrganizationPolicySql() {
return `
CREATE POLICY "organizations_self_or_global"
ON "organizations"
FOR ALL
USING (
COALESCE(current_setting('app.global_access', true), 'false') = 'true'
OR "id" = NULLIF(current_setting('app.current_org', true), '')::uuid
)
WITH CHECK (
COALESCE(current_setting('app.global_access', true), 'false') = 'true'
OR "id" = NULLIF(current_setting('app.current_org', true), '')::uuid
)
`;
}
module.exports = {
async up(queryInterface, Sequelize) {
const transaction = await queryInterface.sequelize.transaction();
try {
await normalizeTenantSubdomains(queryInterface, Sequelize, transaction);
await queryInterface.sequelize.query(
'DROP INDEX IF EXISTS "tenants_subdomain_unique_active"',
{ transaction },
);
await queryInterface.sequelize.query(
'CREATE UNIQUE INDEX "tenants_subdomain_unique_active" ON "tenants" ("subdomain") WHERE "deletedAt" IS NULL',
{ transaction },
);
for (const tableName of ORG_SCOPED_TABLES) {
await queryInterface.sequelize.query(
`ALTER TABLE "${tableName}" ENABLE ROW LEVEL SECURITY`,
{ transaction },
);
await queryInterface.sequelize.query(
`ALTER TABLE "${tableName}" FORCE ROW LEVEL SECURITY`,
{ transaction },
);
await queryInterface.sequelize.query(
`DROP POLICY IF EXISTS "${tableName}_organization_isolation" ON "${tableName}"`,
{ transaction },
);
await queryInterface.sequelize.query(buildScopedPolicySql(tableName), {
transaction,
});
}
await queryInterface.sequelize.query(
'ALTER TABLE "organizations" ENABLE ROW LEVEL SECURITY',
{ transaction },
);
await queryInterface.sequelize.query(
'ALTER TABLE "organizations" FORCE ROW LEVEL SECURITY',
{ transaction },
);
await queryInterface.sequelize.query(
'DROP POLICY IF EXISTS "organizations_self_or_global" ON "organizations"',
{ transaction },
);
await queryInterface.sequelize.query(buildOrganizationPolicySql(), {
transaction,
});
await queryInterface.sequelize.query(
`
UPDATE "users"
SET "app_roleId" = super_admin.id,
"updatedAt" = NOW()
FROM (
SELECT id
FROM "roles"
WHERE name = 'Super Administrator'
LIMIT 1
) AS super_admin
WHERE "users"."email" = 'admin@flatlogic.com'
`,
{ transaction },
);
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
},
async down(queryInterface) {
const transaction = await queryInterface.sequelize.transaction();
try {
await queryInterface.sequelize.query(
'DROP INDEX IF EXISTS "tenants_subdomain_unique_active"',
{ transaction },
);
for (const tableName of ORG_SCOPED_TABLES) {
await queryInterface.sequelize.query(
`DROP POLICY IF EXISTS "${tableName}_organization_isolation" ON "${tableName}"`,
{ transaction },
);
await queryInterface.sequelize.query(
`ALTER TABLE "${tableName}" DISABLE ROW LEVEL SECURITY`,
{ transaction },
);
await queryInterface.sequelize.query(
`ALTER TABLE "${tableName}" NO FORCE ROW LEVEL SECURITY`,
{ transaction },
);
}
await queryInterface.sequelize.query(
'DROP POLICY IF EXISTS "organizations_self_or_global" ON "organizations"',
{ transaction },
);
await queryInterface.sequelize.query(
'ALTER TABLE "organizations" DISABLE ROW LEVEL SECURITY',
{ transaction },
);
await queryInterface.sequelize.query(
'ALTER TABLE "organizations" NO FORCE ROW LEVEL SECURITY',
{ transaction },
);
await queryInterface.sequelize.query(
`
UPDATE "users"
SET "app_roleId" = administrator.id,
"updatedAt" = NOW()
FROM (
SELECT id
FROM "roles"
WHERE name = 'Administrator'
LIMIT 1
) AS administrator
WHERE "users"."email" = 'admin@flatlogic.com'
`,
{ transaction },
);
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
},
};

View File

@ -0,0 +1,99 @@
'use strict';
const PUBLIC_CONTEXT_TABLES = ['users', 'tenants', 'organizations'];
module.exports = {
async up(queryInterface) {
const transaction = await queryInterface.sequelize.transaction();
try {
await queryInterface.sequelize.query(
'DROP POLICY IF EXISTS "organizations_self_or_global" ON "organizations"',
{ transaction },
);
for (const tableName of PUBLIC_CONTEXT_TABLES) {
await queryInterface.sequelize.query(
`DROP POLICY IF EXISTS "${tableName}_organization_isolation" ON "${tableName}"`,
{ transaction },
);
await queryInterface.sequelize.query(
`ALTER TABLE "${tableName}" NO FORCE ROW LEVEL SECURITY`,
{ transaction },
);
await queryInterface.sequelize.query(
`ALTER TABLE "${tableName}" DISABLE ROW LEVEL SECURITY`,
{ transaction },
);
}
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
},
async down(queryInterface) {
const transaction = await queryInterface.sequelize.transaction();
try {
await queryInterface.sequelize.query(
'ALTER TABLE "organizations" ENABLE ROW LEVEL SECURITY',
{ transaction },
);
await queryInterface.sequelize.query(
'ALTER TABLE "organizations" FORCE ROW LEVEL SECURITY',
{ transaction },
);
await queryInterface.sequelize.query(
`
CREATE POLICY "organizations_self_or_global"
ON "organizations"
FOR ALL
USING (
COALESCE(current_setting('app.global_access', true), 'false') = 'true'
OR "id" = NULLIF(current_setting('app.current_org', true), '')::uuid
)
WITH CHECK (
COALESCE(current_setting('app.global_access', true), 'false') = 'true'
OR "id" = NULLIF(current_setting('app.current_org', true), '')::uuid
)
`,
{ transaction },
);
for (const tableName of ['users', 'tenants']) {
await queryInterface.sequelize.query(
`ALTER TABLE "${tableName}" ENABLE ROW LEVEL SECURITY`,
{ transaction },
);
await queryInterface.sequelize.query(
`ALTER TABLE "${tableName}" FORCE ROW LEVEL SECURITY`,
{ transaction },
);
await queryInterface.sequelize.query(
`
CREATE POLICY "${tableName}_organization_isolation"
ON "${tableName}"
FOR ALL
USING (
COALESCE(current_setting('app.global_access', true), 'false') = 'true'
OR "organizationsId" = NULLIF(current_setting('app.current_org', true), '')::uuid
)
WITH CHECK (
COALESCE(current_setting('app.global_access', true), 'false') = 'true'
OR "organizationsId" = NULLIF(current_setting('app.current_org', true), '')::uuid
)
`,
{ transaction },
);
}
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
},
};

View File

@ -5,7 +5,9 @@ const path = require('path');
const Sequelize = require('sequelize');
const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = require("../db.config")[env];
const config = require('../db.config')[env];
const { getRequestContext } = require('../../requestContext');
const { normalizeSubdomain, validateSubdomainOrThrow } = require('../../tenantSubdomain');
const db = {};
let sequelize;
@ -16,22 +18,139 @@ if (config.use_env_variable) {
sequelize = new Sequelize(config.database, config.username, config.password, config);
}
const Op = Sequelize.Op;
function addWhereCondition(options, condition) {
if (!condition) {
return;
}
if (!options.where) {
options.where = condition;
return;
}
options.where = {
[Op.and]: [options.where, condition],
};
}
function getOrganizationScopeCondition(model, context) {
if (!context.enforceOrganizationScope || context.globalAccess) {
return null;
}
if (model?.name === 'organizations') {
if (!context.currentOrg) {
return Sequelize.literal('1 = 0');
}
return {
id: context.currentOrg,
};
}
if (!model?.rawAttributes?.organizationsId) {
return null;
}
if (!context.currentOrg) {
return Sequelize.literal('1 = 0');
}
return {
organizationsId: context.currentOrg,
};
}
function applyOrganizationScope(model, options = {}) {
const context = getRequestContext();
const condition = getOrganizationScopeCondition(model, context);
if (!condition) {
return options;
}
addWhereCondition(options, condition);
return options;
}
function enforceOrganizationWriteScope(model, instance) {
const context = getRequestContext();
if (!context.enforceOrganizationScope || context.globalAccess) {
return;
}
if (model?.rawAttributes?.organizationsId) {
if (!context.currentOrg) {
const error = new Error('Authenticated requests must be scoped to an organization.');
error.code = 403;
throw error;
}
instance.set('organizationsId', context.currentOrg);
}
}
sequelize.afterPoolAcquire(async (connection) => {
const context = getRequestContext();
const currentOrg = context.currentOrg || '';
const globalAccess = context.globalAccess ? 'true' : 'false';
await connection.query(
'select set_config($1, $2, false), set_config($3, $4, false)',
['app.current_org', currentOrg, 'app.global_access', globalAccess],
);
});
fs
.readdirSync(__dirname)
.filter(file => {
return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
.filter((file) => {
return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js';
})
.forEach(file => {
const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes)
.forEach((file) => {
const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);
db[model.name] = model;
});
Object.keys(db).forEach(modelName => {
Object.keys(db).forEach((modelName) => {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
Object.values(db).forEach((model) => {
if (!model?.addHook) {
return;
}
model.addHook('beforeFind', (options) => applyOrganizationScope(model, options));
model.addHook('beforeCount', (options) => applyOrganizationScope(model, options));
model.addHook('beforeCreate', (instance) => enforceOrganizationWriteScope(model, instance));
model.addHook('beforeUpdate', (instance) => enforceOrganizationWriteScope(model, instance));
model.addHook('beforeBulkCreate', (instances) => {
instances.forEach((instance) => enforceOrganizationWriteScope(model, instance));
});
});
if (db.tenants?.addHook) {
db.tenants.addHook('beforeValidate', (tenant) => {
if (tenant.subdomain === undefined || tenant.subdomain === null) {
return;
}
const normalized = normalizeSubdomain(tenant.subdomain);
if (!normalized) {
tenant.set('subdomain', normalized);
return;
}
tenant.set('subdomain', validateSubdomainOrThrow(normalized));
});
}
db.sequelize = sequelize;
db.Sequelize = Sequelize;

View File

@ -6,16 +6,17 @@ const passport = require('passport');
const path = require('path');
const fs = require('fs');
const bodyParser = require('body-parser');
const db = require('./db/models');
const config = require('./config');
const swaggerUI = require('swagger-ui-express');
const swaggerJsDoc = require('swagger-jsdoc');
const { initializeRequestContext, attachAuthenticatedRequestContext } = require('./requestContext');
const authRoutes = require('./routes/auth');
const fileRoutes = require('./routes/file');
const searchRoutes = require('./routes/search');
const sqlRoutes = require('./routes/sql');
const pexelsRoutes = require('./routes/pexels');
const contactFormRoutes = require('./routes/contactForm');
const organizationForAuthRoutes = require('./routes/organizationLogin');
@ -160,123 +161,68 @@ app.use(cors({origin: true}));
require('./auth/auth');
app.use(bodyParser.json());
app.use(initializeRequestContext);
app.use('/api/auth', authRoutes);
app.use('/api/file', fileRoutes);
app.use('/api/pexels', pexelsRoutes);
app.use('/api/contact-form', contactFormRoutes);
app.use('/api/org-for-auth', organizationForAuthRoutes);
app.enable('trust proxy');
app.use('/api/users', passport.authenticate('jwt', {session: false}), usersRoutes);
app.use('/api/roles', passport.authenticate('jwt', {session: false}), rolesRoutes);
app.use('/api/permissions', passport.authenticate('jwt', {session: false}), permissionsRoutes);
app.use('/api/organizations', passport.authenticate('jwt', {session: false}), organizationsRoutes);
app.use('/api/tenants', passport.authenticate('jwt', {session: false}), tenantsRoutes);
app.use('/api/service_lines', passport.authenticate('jwt', {session: false}), service_linesRoutes);
app.use('/api/services', passport.authenticate('jwt', {session: false}), servicesRoutes);
app.use('/api/companies', passport.authenticate('jwt', {session: false}), companiesRoutes);
app.use('/api/contacts', passport.authenticate('jwt', {session: false}), contactsRoutes);
app.use('/api/tags', passport.authenticate('jwt', {session: false}), tagsRoutes);
app.use('/api/pipelines', passport.authenticate('jwt', {session: false}), pipelinesRoutes);
app.use('/api/deal_stages', passport.authenticate('jwt', {session: false}), deal_stagesRoutes);
app.use('/api/leads', passport.authenticate('jwt', {session: false}), leadsRoutes);
app.use('/api/deals', passport.authenticate('jwt', {session: false}), dealsRoutes);
app.use('/api/project_templates', passport.authenticate('jwt', {session: false}), project_templatesRoutes);
app.use('/api/projects', passport.authenticate('jwt', {session: false}), projectsRoutes);
app.use('/api/milestones', passport.authenticate('jwt', {session: false}), milestonesRoutes);
app.use('/api/tasks', passport.authenticate('jwt', {session: false}), tasksRoutes);
app.use('/api/task_dependencies', passport.authenticate('jwt', {session: false}), task_dependenciesRoutes);
app.use('/api/deliverables', passport.authenticate('jwt', {session: false}), deliverablesRoutes);
app.use('/api/project_risks', passport.authenticate('jwt', {session: false}), project_risksRoutes);
app.use('/api/project_status_reports', passport.authenticate('jwt', {session: false}), project_status_reportsRoutes);
app.use('/api/skills', passport.authenticate('jwt', {session: false}), skillsRoutes);
app.use('/api/team_members', passport.authenticate('jwt', {session: false}), team_membersRoutes);
app.use('/api/team_member_skills', passport.authenticate('jwt', {session: false}), team_member_skillsRoutes);
app.use('/api/resource_allocations', passport.authenticate('jwt', {session: false}), resource_allocationsRoutes);
app.use('/api/leave_requests', passport.authenticate('jwt', {session: false}), leave_requestsRoutes);
app.use('/api/public_holidays', passport.authenticate('jwt', {session: false}), public_holidaysRoutes);
app.use('/api/timesheets', passport.authenticate('jwt', {session: false}), timesheetsRoutes);
app.use('/api/time_entries', passport.authenticate('jwt', {session: false}), time_entriesRoutes);
app.use('/api/products', passport.authenticate('jwt', {session: false}), productsRoutes);
app.use('/api/contracts', passport.authenticate('jwt', {session: false}), contractsRoutes);
app.use('/api/invoices', passport.authenticate('jwt', {session: false}), invoicesRoutes);
app.use('/api/invoice_line_items', passport.authenticate('jwt', {session: false}), invoice_line_itemsRoutes);
app.use('/api/payments', passport.authenticate('jwt', {session: false}), paymentsRoutes);
app.use('/api/support_slas', passport.authenticate('jwt', {session: false}), support_slasRoutes);
app.use('/api/support_tickets', passport.authenticate('jwt', {session: false}), support_ticketsRoutes);
app.use('/api/ticket_comments', passport.authenticate('jwt', {session: false}), ticket_commentsRoutes);
app.use('/api/csat_surveys', passport.authenticate('jwt', {session: false}), csat_surveysRoutes);
app.use('/api/email_templates', passport.authenticate('jwt', {session: false}), email_templatesRoutes);
app.use('/api/notifications', passport.authenticate('jwt', {session: false}), notificationsRoutes);
app.use('/api/audit_logs', passport.authenticate('jwt', {session: false}), audit_logsRoutes);
app.use('/api/imports', passport.authenticate('jwt', {session: false}), importsRoutes);
app.use(
'/api/openai',
passport.authenticate('jwt', { session: false }),
openaiRoutes,
);
app.use(
'/api/ai',
passport.authenticate('jwt', { session: false }),
openaiRoutes,
'/api',
passport.authenticate('jwt', { session: false }),
attachAuthenticatedRequestContext,
);
app.use(
'/api/search',
passport.authenticate('jwt', { session: false }),
searchRoutes);
app.use(
'/api/sql',
passport.authenticate('jwt', { session: false }),
sqlRoutes);
app.use(
'/api/org-for-auth',
organizationForAuthRoutes,
);
app.use('/api/users', usersRoutes);
app.use('/api/roles', rolesRoutes);
app.use('/api/permissions', permissionsRoutes);
app.use('/api/organizations', organizationsRoutes);
app.use('/api/tenants', tenantsRoutes);
app.use('/api/service_lines', service_linesRoutes);
app.use('/api/services', servicesRoutes);
app.use('/api/companies', companiesRoutes);
app.use('/api/contacts', contactsRoutes);
app.use('/api/tags', tagsRoutes);
app.use('/api/pipelines', pipelinesRoutes);
app.use('/api/deal_stages', deal_stagesRoutes);
app.use('/api/leads', leadsRoutes);
app.use('/api/deals', dealsRoutes);
app.use('/api/project_templates', project_templatesRoutes);
app.use('/api/projects', projectsRoutes);
app.use('/api/milestones', milestonesRoutes);
app.use('/api/tasks', tasksRoutes);
app.use('/api/task_dependencies', task_dependenciesRoutes);
app.use('/api/deliverables', deliverablesRoutes);
app.use('/api/project_risks', project_risksRoutes);
app.use('/api/project_status_reports', project_status_reportsRoutes);
app.use('/api/skills', skillsRoutes);
app.use('/api/team_members', team_membersRoutes);
app.use('/api/team_member_skills', team_member_skillsRoutes);
app.use('/api/resource_allocations', resource_allocationsRoutes);
app.use('/api/leave_requests', leave_requestsRoutes);
app.use('/api/public_holidays', public_holidaysRoutes);
app.use('/api/timesheets', timesheetsRoutes);
app.use('/api/time_entries', time_entriesRoutes);
app.use('/api/products', productsRoutes);
app.use('/api/contracts', contractsRoutes);
app.use('/api/invoices', invoicesRoutes);
app.use('/api/invoice_line_items', invoice_line_itemsRoutes);
app.use('/api/payments', paymentsRoutes);
app.use('/api/support_slas', support_slasRoutes);
app.use('/api/support_tickets', support_ticketsRoutes);
app.use('/api/ticket_comments', ticket_commentsRoutes);
app.use('/api/csat_surveys', csat_surveysRoutes);
app.use('/api/email_templates', email_templatesRoutes);
app.use('/api/notifications', notificationsRoutes);
app.use('/api/audit_logs', audit_logsRoutes);
app.use('/api/imports', importsRoutes);
app.use('/api/openai', openaiRoutes);
app.use('/api/ai', openaiRoutes);
app.use('/api/search', searchRoutes);
app.use('/api/sql', sqlRoutes);
const publicDir = path.join(

View File

@ -0,0 +1,55 @@
const { AsyncLocalStorage } = require('async_hooks');
const requestContextStorage = new AsyncLocalStorage();
function initializeRequestContext(req, res, next) {
requestContextStorage.run(
{
currentOrg: null,
currentUserId: null,
globalAccess: false,
enforceOrganizationScope: false,
},
next,
);
}
function getRequestContext() {
return (
requestContextStorage.getStore() || {
currentOrg: null,
currentUserId: null,
globalAccess: false,
enforceOrganizationScope: false,
}
);
}
function setRequestContextScope({ currentOrg = null, currentUserId = null, globalAccess = false, enforceOrganizationScope = false }) {
const context = getRequestContext();
context.currentOrg = currentOrg;
context.currentUserId = currentUserId;
context.globalAccess = Boolean(globalAccess);
context.enforceOrganizationScope = Boolean(enforceOrganizationScope);
return context;
}
function attachAuthenticatedRequestContext(req, res, next) {
setRequestContextScope({
currentOrg: req.currentUser?.organizationsId || null,
currentUserId: req.currentUser?.id || null,
globalAccess: req.currentUser?.app_role?.globalAccess,
enforceOrganizationScope: true,
});
next();
}
module.exports = {
initializeRequestContext,
getRequestContext,
setRequestContextScope,
attachAuthenticatedRequestContext,
};

View File

@ -393,7 +393,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await Audit_logsDBApi.findAllAutocomplete(
@ -441,7 +441,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await Audit_logsDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -6,6 +6,7 @@ const AuthService = require('../services/auth');
const ForbiddenError = require('../services/notifications/errors/forbidden');
const EmailSender = require('../services/email');
const wrapAsync = require('../helpers').wrapAsync;
const { resolveTenantContextByHost } = require('../tenantContext');
const router = express.Router();
@ -140,13 +141,21 @@ router.post('/send-password-reset-email', wrapAsync(async (req, res) => {
*/
router.post('/signup', wrapAsync(async (req, res) => {
const link = new URL(req.headers.referer);
const referer = req.headers.referer || `${req.protocol}://${req.get('host')}`;
const link = new URL(referer);
const { organization } = await resolveTenantContextByHost(req, { required: true });
const requestedOrganizationId = req.body.organizationsId || req.body.organizationId || null;
if (requestedOrganizationId && requestedOrganizationId !== organization.id) {
const error = new Error('Signup organization does not match the current tenant context.');
error.code = 400;
throw error;
}
const payload = await AuthService.signup(
req.body.email,
req.body.password,
req.body.organizationId,
organization.id,
req,
link.host,
)

View File

@ -412,7 +412,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await CompaniesDBApi.findAllAutocomplete(
@ -460,7 +460,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await CompaniesDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -0,0 +1,753 @@
const express = require('express');
const db = require('../db/models');
const wrapAsync = require('../helpers').wrapAsync;
const { resolveTenantContextByHost } = require('../tenantContext');
const { setRequestContextScope } = require('../requestContext');
const router = express.Router();
const { Op } = db.Sequelize;
const SERVICE_CATALOG = {
ai_ml: {
lineNameFr: 'IA & Machine Learning',
lineNameEn: 'AI & Machine Learning',
lineDescriptionFr:
'Automatisation, analytics et modèles sur mesure pour les entreprises africaines.',
lineDescriptionEn:
'Custom AI, analytics, and automation for African enterprises.',
pricingModel: 'project_fixed',
typicalTimeline: '16 semaines',
serviceNameFr: 'Implémentation IA',
serviceNameEn: 'AI Implementation',
serviceDescriptionFr:
'Discovery, préparation des données, modélisation, intégration et transfert.',
serviceDescriptionEn:
'Discovery, data preparation, modeling, integration and handover.',
templateName: 'AI Implementation',
templateDescription:
'Discovery → Data → Model → Integration → Training → Handover',
templateWeeks: 16,
billingModel: 'fixed_price',
},
cybersecurity: {
lineNameFr: 'Cybersécurité',
lineNameEn: 'Cybersecurity',
lineDescriptionFr:
'Audits, remédiation et supervision pour sécuriser les SI critiques.',
lineDescriptionEn:
'Audits, remediation, and monitoring for critical information systems.',
pricingModel: 'project_fixed',
typicalTimeline: '4 semaines',
serviceNameFr: 'Audit cybersécurité',
serviceNameEn: 'Cybersecurity Audit',
serviceDescriptionFr:
'Scoping, assessment, remediation planning et reporting exécutif.',
serviceDescriptionEn:
'Scoping, assessment, remediation planning, and executive reporting.',
templateName: 'Cybersecurity Audit',
templateDescription: 'Scoping → Assessment → Remediation → Report',
templateWeeks: 4,
billingModel: 'fixed_price',
},
cloud: {
lineNameFr: 'Cloud Solutions',
lineNameEn: 'Cloud Solutions',
lineDescriptionFr:
'Architecture, migration et opérations cloud pour des équipes agiles.',
lineDescriptionEn:
'Architecture, migration, and cloud operations for agile teams.',
pricingModel: 'hybrid',
typicalTimeline: '8 semaines',
serviceNameFr: 'Migration cloud',
serviceNameEn: 'Cloud Migration',
serviceDescriptionFr:
'Audit, architecture cible, migration et validation des workloads.',
serviceDescriptionEn:
'Audit, target architecture, workload migration, and validation.',
templateName: 'Cloud Migration',
templateDescription: 'Audit → Architecture → Migration → Validation',
templateWeeks: 8,
billingModel: 'time_material',
},
hardware: {
lineNameFr: 'Infrastructure matérielle',
lineNameEn: 'Hardware Infrastructure',
lineDescriptionFr:
'Réseaux, équipements et déploiements terrain avec expertise locale.',
lineDescriptionEn:
'Networks, equipment, and field deployments with local expertise.',
pricingModel: 'project_fixed',
typicalTimeline: 'Variable',
serviceNameFr: 'Déploiement hardware',
serviceNameEn: 'Hardware Deployment',
serviceDescriptionFr:
'Approvisionnement, installation, tests et mise en production.',
serviceDescriptionEn:
'Procurement, installation, testing, and go-live.',
templateName: 'Hardware Deployment',
templateDescription: 'Procurement → Install → Test → Handover',
templateWeeks: 6,
billingModel: 'fixed_price',
},
web_mobile: {
lineNameFr: 'Web & Mobile',
lineNameEn: 'Web & Mobile',
lineDescriptionFr:
'Produits digitaux sur mesure, du cadrage jusquau déploiement.',
lineDescriptionEn:
'Custom digital products from discovery through deployment.',
pricingModel: 'hybrid',
typicalTimeline: '12 semaines',
serviceNameFr: 'Développement web / mobile',
serviceNameEn: 'Web / Mobile Development',
serviceDescriptionFr:
'Sprints agiles, UX, développement et stabilisation continue.',
serviceDescriptionEn:
'Agile sprints, UX, development, and continuous hardening.',
templateName: 'Web/Mobile Development',
templateDescription: 'Sprint setup → Build → QA → Launch',
templateWeeks: 12,
billingModel: 'time_material',
},
database: {
lineNameFr: 'Database Management',
lineNameEn: 'Database Management',
lineDescriptionFr:
'Architecture, migration et optimisation de données à grande échelle.',
lineDescriptionEn:
'Architecture, migration, and optimization for scalable data systems.',
pricingModel: 'project_fixed',
typicalTimeline: '6 semaines',
serviceNameFr: 'Setup base de données',
serviceNameEn: 'Database Setup',
serviceDescriptionFr:
'Modélisation, setup, migration et tuning des performances.',
serviceDescriptionEn:
'Modeling, setup, migration, and performance tuning.',
templateName: 'Database Setup',
templateDescription: 'Modeling → Setup → Migration → Optimization',
templateWeeks: 6,
billingModel: 'fixed_price',
},
};
const COMPANY_SIZE_LABELS = {
'1_10': '110 employés',
'11_50': '1150 employés',
'51_200': '51200 employés',
'201_500': '201500 employés',
'501_1000': '5011000 employés',
'1000_plus': '1000+ employés',
};
const BUDGET_RANGE_LABELS = {
lt_5k: '< 5 000 USD',
'5_20k': '5 00020 000 USD',
'20_50k': '20 00050 000 USD',
'50k_plus': '50 000+ USD',
};
const BUDGET_ESTIMATES = {
lt_5k: 3000,
'5_20k': 12500,
'20_50k': 35000,
'50k_plus': 60000,
};
const CHANNEL_LABELS = {
email: 'Email',
phone: 'Téléphone',
whatsapp: 'WhatsApp',
};
const URGENCY_LABELS = {
standard: 'Standard',
urgent: 'Urgent',
critical: 'Critique',
};
const SALES_STAGE_BLUEPRINT = [
{
name: 'Nouveau besoin',
probability_percent: 10,
sort_order: 1,
is_closed_won: false,
is_closed_lost: false,
},
{
name: 'Qualification',
probability_percent: 30,
sort_order: 2,
is_closed_won: false,
is_closed_lost: false,
},
{
name: 'Proposition envoyée',
probability_percent: 60,
sort_order: 3,
is_closed_won: false,
is_closed_lost: false,
},
{
name: 'Négociation',
probability_percent: 80,
sort_order: 4,
is_closed_won: false,
is_closed_lost: false,
},
];
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
function normalizeText(value) {
if (typeof value !== 'string') {
return '';
}
return value.trim().replace(/\s+/g, ' ');
}
function throwBadRequest(message) {
const error = new Error(message);
error.code = 400;
throw error;
}
function splitFullName(fullName) {
const parts = fullName.split(' ').filter(Boolean);
return {
firstName: parts[0] || fullName,
lastName: parts.slice(1).join(' ') || '-',
};
}
function getBudgetEstimate(range) {
return BUDGET_ESTIMATES[range] || null;
}
function getScore(payload) {
let score = 35;
if (['11_50', '51_200', '201_500', '501_1000', '1000_plus'].includes(payload.company_size)) {
score += 10;
}
if (['51_200', '201_500', '501_1000', '1000_plus'].includes(payload.company_size)) {
score += 5;
}
if (payload.budget_range === '5_20k') {
score += 10;
}
if (payload.budget_range === '20_50k') {
score += 15;
}
if (payload.budget_range === '50k_plus') {
score += 20;
}
if (['ai_ml', 'cybersecurity', 'cloud'].includes(payload.service_line)) {
score += 10;
}
if (payload.urgency === 'urgent') {
score += 5;
}
if (payload.urgency === 'critical') {
score += 10;
}
if (payload.preferred_channel === 'whatsapp') {
score += 5;
}
if (payload.message.length > 120) {
score += 5;
}
return Math.min(score, 100);
}
function getScope(tenant, organization) {
const scope = {};
if (tenant?.id) {
scope.tenantId = tenant.id;
}
if (organization?.id) {
scope.organizationsId = organization.id;
}
return scope;
}
function addDays(days) {
const date = new Date();
date.setDate(date.getDate() + days);
return date;
}
function buildLeadMessage(payload, catalog) {
return [
`Service demandé: ${catalog.lineNameFr}`,
`Taille du client: ${COMPANY_SIZE_LABELS[payload.company_size]}`,
`Budget estimé: ${BUDGET_RANGE_LABELS[payload.budget_range]}`,
`Canal préféré: ${CHANNEL_LABELS[payload.preferred_channel]}`,
`Urgence: ${URGENCY_LABELS[payload.urgency]}`,
'',
payload.message,
].join('\n');
}
function buildCompanyNote(payload) {
return [
'Fiche enrichie automatiquement depuis la landing page publique TSC FleetOS.',
`Taille: ${COMPANY_SIZE_LABELS[payload.company_size]}`,
`Dernier canal préféré: ${CHANNEL_LABELS[payload.preferred_channel]}`,
].join('\n');
}
function buildContactNote(payload) {
return [
'Lead entrant web.',
`Canal préféré: ${CHANNEL_LABELS[payload.preferred_channel]}`,
`Urgence: ${URGENCY_LABELS[payload.urgency]}`,
].join('\n');
}
function buildDealNote(payload, score, lead, catalog) {
return [
`Référence lead: TSC-${lead.id.split('-')[0].toUpperCase()}`,
`Score: ${score}/100`,
`Service recommandé: ${catalog.serviceNameFr}`,
`Template suggéré: ${catalog.templateName}`,
`Budget visé: ${BUDGET_RANGE_LABELS[payload.budget_range]}`,
`Canal préféré: ${CHANNEL_LABELS[payload.preferred_channel]}`,
`Urgence: ${URGENCY_LABELS[payload.urgency]}`,
'',
payload.message,
].join('\n');
}
async function attachScope(instance, tenant, organization, transaction) {
if (tenant && typeof instance.setTenant === 'function') {
await instance.setTenant(tenant, { transaction });
}
if (organization && typeof instance.setOrganizations === 'function') {
await instance.setOrganizations(organization, { transaction });
}
}
async function resolveTenantContext(req, transaction) {
return resolveTenantContextByHost(req, {
transaction,
required: true,
});
}
async function ensureSalesPipeline(tenant, organization, transaction) {
const scope = getScope(tenant, organization);
let pipeline = await db.pipelines.findOne({
where: {
pipeline_type: 'sales',
...scope,
},
order: [
['sort_order', 'ASC'],
['createdAt', 'ASC'],
],
transaction,
});
if (!pipeline) {
pipeline = await db.pipelines.create(
{
name: 'Pipeline commercial TSC',
pipeline_type: 'sales',
active: true,
sort_order: 1,
},
{ transaction },
);
await attachScope(pipeline, tenant, organization, transaction);
}
let firstStage = null;
for (const stageBlueprint of SALES_STAGE_BLUEPRINT) {
let stage = await db.deal_stages.findOne({
where: {
pipelineId: pipeline.id,
name: stageBlueprint.name,
...scope,
},
transaction,
});
if (!stage) {
stage = await db.deal_stages.create(stageBlueprint, { transaction });
await attachScope(stage, tenant, organization, transaction);
await stage.setPipeline(pipeline, { transaction });
}
if (!firstStage || stage.sort_order < firstStage.sort_order) {
firstStage = stage;
}
}
return {
pipeline,
firstStage,
};
}
async function ensureCatalog(serviceLineKey, tenant, organization, transaction) {
const catalog = SERVICE_CATALOG[serviceLineKey];
if (!catalog) {
throwBadRequest('Service line invalide.');
}
const scope = getScope(tenant, organization);
let serviceLine = await db.service_lines.findOne({
where: {
category: serviceLineKey,
...scope,
},
transaction,
});
if (!serviceLine) {
serviceLine = await db.service_lines.create(
{
name_fr: catalog.lineNameFr,
name_en: catalog.lineNameEn,
description_fr: catalog.lineDescriptionFr,
description_en: catalog.lineDescriptionEn,
category: serviceLineKey,
pricing_model: catalog.pricingModel,
typical_timeline: catalog.typicalTimeline,
active: true,
},
{ transaction },
);
await attachScope(serviceLine, tenant, organization, transaction);
}
let service = await db.services.findOne({
where: {
service_lineId: serviceLine.id,
name_fr: catalog.serviceNameFr,
...scope,
},
transaction,
});
if (!service) {
service = await db.services.create(
{
name_fr: catalog.serviceNameFr,
name_en: catalog.serviceNameEn,
description_fr: catalog.serviceDescriptionFr,
description_en: catalog.serviceDescriptionEn,
pricing_model: catalog.pricingModel,
min_price: getBudgetEstimate('lt_5k'),
max_price: getBudgetEstimate('50k_plus'),
typical_duration_weeks: catalog.templateWeeks,
active: true,
},
{ transaction },
);
await attachScope(service, tenant, organization, transaction);
await service.setService_line(serviceLine, { transaction });
}
let projectTemplate = await db.project_templates.findOne({
where: {
service_lineId: serviceLine.id,
name: catalog.templateName,
...scope,
},
transaction,
});
if (!projectTemplate) {
projectTemplate = await db.project_templates.create(
{
name: catalog.templateName,
description: catalog.templateDescription,
default_duration_weeks: catalog.templateWeeks,
default_budget: getBudgetEstimate('20_50k'),
billing_model: catalog.billingModel,
active: true,
},
{ transaction },
);
await attachScope(projectTemplate, tenant, organization, transaction);
await projectTemplate.setService_line(serviceLine, { transaction });
}
return {
serviceLine,
service,
projectTemplate,
...catalog,
};
}
function validatePayload(payload) {
if (!payload.full_name) {
throwBadRequest('Le nom du contact est obligatoire.');
}
if (!payload.company_name) {
throwBadRequest('Le nom de la société est obligatoire.');
}
if (!payload.email || !emailPattern.test(payload.email)) {
throwBadRequest('Veuillez fournir un email professionnel valide.');
}
if (!SERVICE_CATALOG[payload.service_line]) {
throwBadRequest('Veuillez sélectionner une ligne de service valide.');
}
if (!COMPANY_SIZE_LABELS[payload.company_size]) {
throwBadRequest('Veuillez sélectionner la taille de votre entreprise.');
}
if (!BUDGET_RANGE_LABELS[payload.budget_range]) {
throwBadRequest('Veuillez sélectionner un budget estimatif.');
}
if (!CHANNEL_LABELS[payload.preferred_channel]) {
throwBadRequest('Veuillez sélectionner un canal de communication valide.');
}
if (!URGENCY_LABELS[payload.urgency]) {
throwBadRequest('Veuillez sélectionner un niveau durgence valide.');
}
if (payload.preferred_channel !== 'email' && !payload.phone) {
throwBadRequest('Le numéro de téléphone est requis pour un suivi par téléphone ou WhatsApp.');
}
if (!payload.message || payload.message.length < 24) {
throwBadRequest('Décrivez votre besoin en au moins 24 caractères.');
}
}
router.post(
'/',
wrapAsync(async (req, res) => {
const payload = {
full_name: normalizeText(req.body.full_name),
company_name: normalizeText(req.body.company_name),
email: normalizeText(req.body.email).toLowerCase(),
phone: normalizeText(req.body.phone),
service_line: normalizeText(req.body.service_line),
company_size: req.body.company_size,
budget_range: req.body.budget_range,
preferred_channel: normalizeText(req.body.preferred_channel).toLowerCase(),
urgency: normalizeText(req.body.urgency).toLowerCase(),
message: normalizeText(req.body.message),
};
validatePayload(payload);
const transaction = await db.sequelize.transaction();
try {
const { tenant, organization } = await resolveTenantContext(req, transaction);
setRequestContextScope({
currentOrg: organization.id,
enforceOrganizationScope: true,
});
await db.sequelize.query(
"select set_config('app.current_org', :currentOrg, false), set_config('app.global_access', 'false', false)",
{
replacements: { currentOrg: organization.id },
transaction,
},
);
const scope = getScope(tenant, organization);
const catalog = await ensureCatalog(payload.service_line, tenant, organization, transaction);
const { pipeline, firstStage } = await ensureSalesPipeline(tenant, organization, transaction);
const { firstName, lastName } = splitFullName(payload.full_name);
const score = getScore(payload);
const leadStatus = score >= 70 ? 'qualified' : 'new';
let company = await db.companies.findOne({
where: {
...scope,
[Op.or]: [
{ legal_name: { [Op.iLike]: payload.company_name } },
{ trade_name: { [Op.iLike]: payload.company_name } },
],
},
transaction,
});
if (!company) {
company = await db.companies.create(
{
legal_name: payload.company_name,
trade_name: payload.company_name,
size_band: payload.company_size,
country: 'DRC',
lifecycle_stage: 'lead',
notes: buildCompanyNote(payload),
},
{ transaction },
);
await attachScope(company, tenant, organization, transaction);
} else {
await company.update(
{
size_band: company.size_band || payload.company_size,
lifecycle_stage: company.lifecycle_stage || 'lead',
notes: buildCompanyNote(payload),
},
{ transaction },
);
}
let contact = await db.contacts.findOne({
where: {
...scope,
email: { [Op.iLike]: payload.email },
},
transaction,
});
const contactPayload = {
first_name: firstName,
last_name: lastName,
email: payload.email,
phone: payload.phone || null,
whatsapp: payload.preferred_channel === 'whatsapp' ? payload.phone || null : null,
preferred_channel: payload.preferred_channel,
allow_email: payload.preferred_channel === 'email',
allow_sms: payload.preferred_channel === 'phone',
allow_whatsapp: payload.preferred_channel === 'whatsapp',
lifecycle_stage: 'lead',
notes: buildContactNote(payload),
};
if (!contact) {
contact = await db.contacts.create(contactPayload, { transaction });
await attachScope(contact, tenant, organization, transaction);
} else {
await contact.update(contactPayload, { transaction });
}
await contact.setCompany(company, { transaction });
let lead = await db.leads.findOne({
where: {
...scope,
email: { [Op.iLike]: payload.email },
company_name: { [Op.iLike]: payload.company_name },
status: {
[Op.in]: ['new', 'qualified'],
},
},
order: [['updatedAt', 'DESC']],
transaction,
});
const leadPayload = {
full_name: payload.full_name,
email: payload.email,
phone: payload.phone || null,
company_name: payload.company_name,
message: buildLeadMessage(payload, catalog),
source: 'website_form',
score,
status: leadStatus,
captured_at: new Date(),
};
if (!lead) {
lead = await db.leads.create(leadPayload, { transaction });
await attachScope(lead, tenant, organization, transaction);
} else {
await lead.update(leadPayload, { transaction });
}
await lead.setService_interest(catalog.service, { transaction });
let deal = await db.deals.findOne({
where: {
...scope,
companyId: company.id,
service_lineId: catalog.serviceLine.id,
status: 'open',
},
order: [['updatedAt', 'DESC']],
transaction,
});
const dealPayload = {
name: `Découverte ${payload.company_name} · ${catalog.serviceNameFr}`,
amount: getBudgetEstimate(payload.budget_range),
currency: 'USD',
expected_close_at: addDays(14),
status: 'open',
notes: buildDealNote(payload, score, lead, catalog),
};
if (!deal) {
deal = await db.deals.create(dealPayload, { transaction });
await attachScope(deal, tenant, organization, transaction);
} else {
await deal.update(dealPayload, { transaction });
}
await deal.setCompany(company, { transaction });
await deal.setPrimary_contact(contact, { transaction });
await deal.setService_line(catalog.serviceLine, { transaction });
await deal.setPipeline(pipeline, { transaction });
if (firstStage) {
await deal.setStage(firstStage, { transaction });
}
await transaction.commit();
res.status(200).send({
success: true,
reference: `TSC-${lead.id.split('-')[0].toUpperCase()}`,
leadId: lead.id,
dealId: deal.id,
score,
status: leadStatus,
stageName: firstStage?.name || 'Nouveau besoin',
pipelineName: pipeline?.name || 'Pipeline commercial TSC',
templateSuggestion: catalog.projectTemplate?.name || catalog.templateName,
});
} catch (error) {
await transaction.rollback();
throw error;
}
}),
);
router.use('/', require('../helpers').commonErrorHandler);
module.exports = router;

View File

@ -405,7 +405,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await ContactsDBApi.findAllAutocomplete(
@ -453,7 +453,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await ContactsDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -393,7 +393,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await ContractsDBApi.findAllAutocomplete(
@ -441,7 +441,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await ContractsDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -383,7 +383,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await Csat_surveysDBApi.findAllAutocomplete(
@ -431,7 +431,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await Csat_surveysDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -386,7 +386,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await Deal_stagesDBApi.findAllAutocomplete(
@ -434,7 +434,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await Deal_stagesDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -391,7 +391,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await DealsDBApi.findAllAutocomplete(
@ -439,7 +439,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await DealsDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -387,7 +387,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await DeliverablesDBApi.findAllAutocomplete(
@ -435,7 +435,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await DeliverablesDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -393,7 +393,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await Email_templatesDBApi.findAllAutocomplete(
@ -441,7 +441,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await Email_templatesDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -391,7 +391,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await ImportsDBApi.findAllAutocomplete(
@ -439,7 +439,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await ImportsDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -389,7 +389,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await Invoice_line_itemsDBApi.findAllAutocomplete(
@ -437,7 +437,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await Invoice_line_itemsDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -404,7 +404,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await InvoicesDBApi.findAllAutocomplete(
@ -452,7 +452,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await InvoicesDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -397,7 +397,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await LeadsDBApi.findAllAutocomplete(
@ -445,7 +445,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await LeadsDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -382,7 +382,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await Leave_requestsDBApi.findAllAutocomplete(
@ -430,7 +430,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await Leave_requestsDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -387,7 +387,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await MilestonesDBApi.findAllAutocomplete(
@ -435,7 +435,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await MilestonesDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -388,7 +388,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await NotificationsDBApi.findAllAutocomplete(
@ -436,7 +436,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await NotificationsDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -1,10 +1,7 @@
const express = require('express');
const OrganizationsDBApi = require('../db/api/organizations');
const wrapAsync = require('../helpers').wrapAsync;
const { resolveTenantContextByHost } = require('../tenantContext');
const router = express.Router();
@ -37,19 +34,21 @@ const router = express.Router();
router.get(
'/',
wrapAsync(async (req, res) => {
const payload = await OrganizationsDBApi.findAll(req.query);
const simplifiedPayload = payload.rows.map(org => ({
id: org.id,
name: org.name
}));
res.status(200).send(simplifiedPayload);
const { organization, tenant, subdomain } = await resolveTenantContextByHost(req, {
required: true,
});
res.status(200).send([
{
id: organization.id,
name: organization.name,
subdomain,
tenantId: tenant.id,
},
]);
}),
);
router.use('/', require('../helpers').commonErrorHandler);
module.exports = router;

View File

@ -380,7 +380,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await OrganizationsDBApi.findAllAutocomplete(
@ -428,7 +428,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await OrganizationsDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -389,7 +389,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await PaymentsDBApi.findAllAutocomplete(
@ -437,7 +437,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await PaymentsDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -417,7 +417,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await PermissionsDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -384,7 +384,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await PipelinesDBApi.findAllAutocomplete(
@ -432,7 +432,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await PipelinesDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -391,7 +391,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await ProductsDBApi.findAllAutocomplete(
@ -439,7 +439,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await ProductsDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -389,7 +389,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await Project_risksDBApi.findAllAutocomplete(
@ -437,7 +437,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await Project_risksDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -390,7 +390,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await Project_status_reportsDBApi.findAllAutocomplete(
@ -438,7 +438,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await Project_status_reportsDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -390,7 +390,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await Project_templatesDBApi.findAllAutocomplete(
@ -438,7 +438,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await Project_templatesDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -402,7 +402,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await ProjectsDBApi.findAllAutocomplete(
@ -450,7 +450,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await ProjectsDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -383,7 +383,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await Public_holidaysDBApi.findAllAutocomplete(
@ -431,7 +431,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await Public_holidaysDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -384,7 +384,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await Resource_allocationsDBApi.findAllAutocomplete(
@ -432,7 +432,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await Resource_allocationsDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -426,7 +426,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await RolesDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -36,8 +36,8 @@ router.use(checkCrudPermissions('search'));
*/
router.post('/', async (req, res) => {
const { searchQuery , organizationId} = req.body;
const { searchQuery } = req.body;
const organizationsId = req.currentUser?.organizationsId || null;
const globalAccess = req.currentUser.app_role.globalAccess;
if (!searchQuery) {
@ -45,7 +45,7 @@ router.post('/', async (req, res) => {
}
try {
const foundMatches = await SearchService.search(searchQuery, req.currentUser , organizationId, globalAccess,);
const foundMatches = await SearchService.search(searchQuery, req.currentUser, organizationsId, globalAccess);
res.json(foundMatches);
} catch (error) {
console.error('Internal Server Error', error);

View File

@ -400,7 +400,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await Service_linesDBApi.findAllAutocomplete(
@ -448,7 +448,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await Service_linesDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -405,7 +405,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await ServicesDBApi.findAllAutocomplete(
@ -453,7 +453,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await ServicesDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -381,7 +381,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await SkillsDBApi.findAllAutocomplete(
@ -429,7 +429,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await SkillsDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -387,7 +387,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await Support_slasDBApi.findAllAutocomplete(
@ -435,7 +435,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await Support_slasDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -389,7 +389,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await Support_ticketsDBApi.findAllAutocomplete(
@ -437,7 +437,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await Support_ticketsDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -383,7 +383,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await TagsDBApi.findAllAutocomplete(
@ -431,7 +431,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await TagsDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -377,7 +377,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await Task_dependenciesDBApi.findAllAutocomplete(
@ -425,7 +425,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await Task_dependenciesDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -394,7 +394,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await TasksDBApi.findAllAutocomplete(
@ -442,7 +442,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await TasksDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -378,7 +378,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await Team_member_skillsDBApi.findAllAutocomplete(
@ -426,7 +426,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await Team_member_skillsDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -389,7 +389,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await Team_membersDBApi.findAllAutocomplete(
@ -437,7 +437,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await Team_membersDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -415,7 +415,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await TenantsDBApi.findAllAutocomplete(
@ -463,7 +463,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await TenantsDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -381,7 +381,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await Ticket_commentsDBApi.findAllAutocomplete(
@ -429,7 +429,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await Ticket_commentsDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -384,7 +384,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await Time_entriesDBApi.findAllAutocomplete(
@ -432,7 +432,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await Time_entriesDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -381,7 +381,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await TimesheetsDBApi.findAllAutocomplete(
@ -429,7 +429,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await TimesheetsDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}

View File

@ -389,7 +389,7 @@ router.get('/autocomplete', async (req, res) => {
const globalAccess = req.currentUser.app_role.globalAccess;
const organizationId = req.currentUser.organization?.id
const organizationId = req.currentUser?.organizationsId
const payload = await UsersDBApi.findAllAutocomplete(
@ -437,7 +437,14 @@ router.get('/autocomplete', async (req, res) => {
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await UsersDBApi.findBy(
{ id: req.params.id },
{ currentUser: req.currentUser },
);
if (!payload) {
const error = new Error('Item not found.');
error.code = 404;
throw error;
}
delete payload.password;

View File

@ -10,8 +10,8 @@ const config = require('../config');
const helpers = require('../helpers');
class Auth {
static async signup(email, password, organizationId, options = {}, host) {
const user = await UsersDBApi.findBy({email});
static async signup(email, password, organizationsId, options = {}, host) {
const user = await UsersDBApi.findAuthUserByEmail(email);
const hashedPassword = await bcrypt.hash(
password,
@ -54,14 +54,18 @@ class Auth {
return helpers.jwtSign(data);
}
if (!organizationsId) {
const error = new Error('A valid tenant organization is required for signup.');
error.code = 400;
throw error;
}
const newUser = await UsersDBApi.createFromAuth(
{
firstName: email.split('@')[0],
password: hashedPassword,
email: email,
organizationId: organizationId,
organizationsId,
},
options,
);
@ -84,7 +88,7 @@ class Auth {
}
static async signin(email, password, options = {}) {
const user = await UsersDBApi.findBy({email});
const user = await UsersDBApi.findAuthUserByEmail(email);
if (!user) {
throw new ValidationError(

View File

@ -24,7 +24,7 @@ function buildWidgetResult(widget, queryResult, queryString) {
async function executeQuery(queryString, currentUser) {
try {
return await db.sequelize.query(queryString, {
replacements: { organizationId: currentUser.organizationId },
replacements: { organizationId: currentUser.organizationsId },
});
} catch (e) {
console.log(e);
@ -49,13 +49,13 @@ function insertWhereConditions(queryString, whereConditions) {
}
function constructWhereConditions(mainTable, currentUser, replacements) {
const { organizationId, app_role: { globalAccess } } = currentUser;
const { organizationsId, app_role: { globalAccess } } = currentUser;
const tablesWithoutOrgId = ['permissions', 'roles'];
let whereConditions = '';
if (!globalAccess && !tablesWithoutOrgId.includes(mainTable)) {
whereConditions += `"${mainTable}"."organizationId" = :organizationId`;
replacements.organizationId = organizationId;
whereConditions += `"${mainTable}"."organizationsId" = :organizationId`;
replacements.organizationId = organizationsId;
}
whereConditions += whereConditions ? ' AND ' : '';
@ -362,7 +362,7 @@ module.exports = class RolesService {
static async getRoleInfoByKey(key, roleId, currentUser) {
const transaction = await db.sequelize.transaction();
const organizationId = currentUser.organizationId;
const organizationId = currentUser.organizationsId;
let globalAccess = currentUser.app_role?.globalAccess;
let queryString = '';

View File

@ -36,7 +36,7 @@ async function checkPermissions(permission, currentUser) {
}
module.exports = class SearchService {
static async search(searchQuery, currentUser , organizationId, globalAccess) {
static async search(searchQuery, currentUser, organizationsId, globalAccess) {
try {
if (!searchQuery) {
throw new ValidationError('iam.errors.searchQueryRequired');
@ -1008,8 +1008,8 @@ module.exports = class SearchService {
};
if (!globalAccess && tableName !== 'organizations' && organizationId) {
whereCondition.organizationId = organizationId;
if (!globalAccess && tableName !== 'organizations' && organizationsId) {
whereCondition.organizationsId = organizationsId;
}

Some files were not shown because too many files have changed in this diff Show More