Merged in feature/IO-3258-Shop-User-Vendor-Creation (pull request #2371)

Feature/IO-3258 Shop User Vendor Creation
This commit is contained in:
Dave Richer
2025-06-09 22:44:01 +00:00
18 changed files with 2146 additions and 392 deletions

119
client/package-lock.json generated
View File

@@ -46,7 +46,7 @@
"logrocket": "^9.0.2",
"markerjs2": "^2.32.4",
"memoize-one": "^6.0.0",
"normalize-url": "^8.0.1",
"normalize-url": "^8.0.2",
"object-hash": "^3.0.0",
"phone": "^3.1.59",
"prop-types": "^15.8.1",
@@ -121,7 +121,7 @@
"vite-plugin-node-polyfills": "^0.23.0",
"vite-plugin-pwa": "^1.0.0",
"vite-plugin-style-import": "^2.0.0",
"vitest": "^3.2.2",
"vitest": "^3.2.3",
"workbox-window": "^7.3.0"
},
"engines": {
@@ -5843,15 +5843,15 @@
}
},
"node_modules/@vitest/expect": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.2.tgz",
"integrity": "sha512-ipHw0z669vEMjzz3xQE8nJX1s0rQIb7oEl4jjl35qWTwm/KIHERIg/p/zORrjAaZKXfsv7IybcNGHwhOOAPMwQ==",
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.3.tgz",
"integrity": "sha512-W2RH2TPWVHA1o7UmaFKISPvdicFJH+mjykctJFoAkUw+SPTJTGjUNdKscFBrqM7IPnCVu6zihtKYa7TkZS1dkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/spy": "3.2.2",
"@vitest/utils": "3.2.2",
"@vitest/spy": "3.2.3",
"@vitest/utils": "3.2.3",
"chai": "^5.2.0",
"tinyrainbow": "^2.0.0"
},
@@ -5860,13 +5860,13 @@
}
},
"node_modules/@vitest/mocker": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.2.tgz",
"integrity": "sha512-jKojcaRyIYpDEf+s7/dD3LJt53c0dPfp5zCPXz9H/kcGrSlovU/t1yEaNzM9oFME3dcd4ULwRI/x0Po1Zf+LTw==",
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.3.tgz",
"integrity": "sha512-cP6fIun+Zx8he4rbWvi+Oya6goKQDZK+Yq4hhlggwQBbrlOQ4qtZ+G4nxB6ZnzI9lyIb+JnvyiJnPC2AGbKSPA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "3.2.2",
"@vitest/spy": "3.2.3",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.17"
},
@@ -5907,9 +5907,9 @@
}
},
"node_modules/@vitest/pretty-format": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.2.tgz",
"integrity": "sha512-FY4o4U1UDhO9KMd2Wee5vumwcaHw7Vg4V7yR4Oq6uK34nhEJOmdRYrk3ClburPRUA09lXD/oXWZ8y/Sdma0aUQ==",
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.3.tgz",
"integrity": "sha512-yFglXGkr9hW/yEXngO+IKMhP0jxyFw2/qys/CK4fFUZnSltD+MU7dVYGrH8rvPcK/O6feXQA+EU33gjaBBbAng==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5920,14 +5920,15 @@
}
},
"node_modules/@vitest/runner": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.2.tgz",
"integrity": "sha512-GYcHcaS3ejGRZYed2GAkvsjBeXIEerDKdX3orQrBJqLRiea4NSS9qvn9Nxmuy1IwIB+EjFOaxXnX79l8HFaBwg==",
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.3.tgz",
"integrity": "sha512-83HWYisT3IpMaU9LN+VN+/nLHVBCSIUKJzGxC5RWUOsK1h3USg7ojL+UXQR3b4o4UBIWCYdD2fxuzM7PQQ1u8w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "3.2.2",
"pathe": "^2.0.3"
"@vitest/utils": "3.2.3",
"pathe": "^2.0.3",
"strip-literal": "^3.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
@@ -5941,13 +5942,13 @@
"license": "MIT"
},
"node_modules/@vitest/snapshot": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.2.tgz",
"integrity": "sha512-aMEI2XFlR1aNECbBs5C5IZopfi5Lb8QJZGGpzS8ZUHML5La5wCbrbhLOVSME68qwpT05ROEEOAZPRXFpxZV2wA==",
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.3.tgz",
"integrity": "sha512-9gIVWx2+tysDqUmmM1L0hwadyumqssOL1r8KJipwLx5JVYyxvVRfxvMq7DaWbZZsCqZnu/dZedaZQh4iYTtneA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "3.2.2",
"@vitest/pretty-format": "3.2.3",
"magic-string": "^0.30.17",
"pathe": "^2.0.3"
},
@@ -5973,9 +5974,9 @@
"license": "MIT"
},
"node_modules/@vitest/spy": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.2.tgz",
"integrity": "sha512-6Utxlx3o7pcTxvp0u8kUiXtRFScMrUg28KjB3R2hon7w4YqOFAEA9QwzPVVS1QNL3smo4xRNOpNZClRVfpMcYg==",
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.3.tgz",
"integrity": "sha512-JHu9Wl+7bf6FEejTCREy+DmgWe+rQKbK+y32C/k5f4TBIAlijhJbRBIRIOCEpVevgRsCQR2iHRUH2/qKVM/plw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5986,13 +5987,13 @@
}
},
"node_modules/@vitest/utils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.2.tgz",
"integrity": "sha512-qJYMllrWpF/OYfWHP32T31QCaLa3BAzT/n/8mNGhPdVcjY+JYazQFO1nsJvXU12Kp1xMpNY4AGuljPTNjQve6A==",
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.3.tgz",
"integrity": "sha512-4zFBCU5Pf+4Z6v+rwnZ1HU1yzOKKvDkMXZrymE2PBlbjKJRlrOxbvpfPSvJTGRIwGoahaOGvp+kbCoxifhzJ1Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "3.2.2",
"@vitest/pretty-format": "3.2.3",
"loupe": "^3.1.3",
"tinyrainbow": "^2.0.0"
},
@@ -12707,9 +12708,9 @@
}
},
"node_modules/normalize-url": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz",
"integrity": "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==",
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.2.tgz",
"integrity": "sha512-Ee/R3SyN4BuynXcnTaekmaVdbDAEiNrHqjQIA37mHU8G9pf7aaAD4ZX3XjBLo6rsdcxA/gtkcNYZLt30ACgynw==",
"license": "MIT",
"engines": {
"node": ">=14.16"
@@ -16299,6 +16300,26 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strip-literal": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz",
"integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==",
"dev": true,
"license": "MIT",
"dependencies": {
"js-tokens": "^9.0.1"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/strip-literal/node_modules/js-tokens": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
"integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
"dev": true,
"license": "MIT"
},
"node_modules/style-to-js": {
"version": "1.1.16",
"resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.16.tgz",
@@ -17576,9 +17597,9 @@
}
},
"node_modules/vite-node": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.2.tgz",
"integrity": "sha512-Xj/jovjZvDXOq2FgLXu8NsY4uHUMWtzVmMC2LkCu9HWdr9Qu1Is5sanX3Z4jOFKdohfaWDnEJWp9pRP0vVpAcA==",
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.3.tgz",
"integrity": "sha512-gc8aAifGuDIpZHrPjuHyP4dpQmYXqWw7D1GmDnWeNWP654UEXzVfQ5IHPSK5HaHkwB/+p1atpYpSdw/2kOv8iQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -17774,20 +17795,20 @@
}
},
"node_modules/vitest": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.2.tgz",
"integrity": "sha512-fyNn/Rp016Bt5qvY0OQvIUCwW2vnaEBLxP42PmKbNIoasSYjML+8xyeADOPvBe+Xfl/ubIw4og7Lt9jflRsCNw==",
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.3.tgz",
"integrity": "sha512-E6U2ZFXe3N/t4f5BwUaVCKRLHqUpk1CBWeMh78UT4VaTPH/2dyvH6ALl29JTovEPu9dVKr/K/J4PkXgrMbw4Ww==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.2",
"@vitest/mocker": "3.2.2",
"@vitest/pretty-format": "^3.2.2",
"@vitest/runner": "3.2.2",
"@vitest/snapshot": "3.2.2",
"@vitest/spy": "3.2.2",
"@vitest/utils": "3.2.2",
"@vitest/expect": "3.2.3",
"@vitest/mocker": "3.2.3",
"@vitest/pretty-format": "^3.2.3",
"@vitest/runner": "3.2.3",
"@vitest/snapshot": "3.2.3",
"@vitest/spy": "3.2.3",
"@vitest/utils": "3.2.3",
"chai": "^5.2.0",
"debug": "^4.4.1",
"expect-type": "^1.2.1",
@@ -17801,7 +17822,7 @@
"tinypool": "^1.1.0",
"tinyrainbow": "^2.0.0",
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
"vite-node": "3.2.2",
"vite-node": "3.2.3",
"why-is-node-running": "^2.3.0"
},
"bin": {
@@ -17817,8 +17838,8 @@
"@edge-runtime/vm": "*",
"@types/debug": "^4.1.12",
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"@vitest/browser": "3.2.2",
"@vitest/ui": "3.2.2",
"@vitest/browser": "3.2.3",
"@vitest/ui": "3.2.3",
"happy-dom": "*",
"jsdom": "*"
},

View File

@@ -45,7 +45,7 @@
"logrocket": "^9.0.2",
"markerjs2": "^2.32.4",
"memoize-one": "^6.0.0",
"normalize-url": "^8.0.1",
"normalize-url": "^8.0.2",
"object-hash": "^3.0.0",
"phone": "^3.1.59",
"prop-types": "^15.8.1",
@@ -161,7 +161,7 @@
"vite-plugin-node-polyfills": "^0.23.0",
"vite-plugin-pwa": "^1.0.0",
"vite-plugin-style-import": "^2.0.0",
"vitest": "^3.2.2",
"vitest": "^3.2.3",
"workbox-window": "^7.3.0"
}
}

View File

@@ -1232,8 +1232,8 @@
"sizelimit": "The selected items exceed the size limit.",
"submit-for-testing": "Error submitting Job for testing.",
"sub_status": {
"expired": "The subscription for this shop has expired. Please contact technical support to reactivate.",
"trial-expired": "The trial for this shop has expired. Please contact technical support to reactivate."
"expired": "The subscription for this shop has expired. Please contact Sales to reactivate.",
"trial-expired": "The trial for this shop has expired. Please contact Sales to reactivate."
}
},
"itemtypes": {

View File

@@ -1035,6 +1035,7 @@
- use_fippa
- use_paint_scale_data
- uselocalmediaserver
- external_shop_id
- website
- workingdays
- zip_post
@@ -1130,6 +1131,7 @@
- use_fippa
- use_paint_scale_data
- uselocalmediaserver
- external_shop_id
- website
- workingdays
- zip_post

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."bodyshops" add column "we_profile_id" text
-- null;

View File

@@ -0,0 +1,2 @@
alter table "public"."bodyshops" add column "we_profile_id" text
null;

View File

@@ -0,0 +1,2 @@
alter table "public"."bodyshops" rename column "parts_management_key" to "we_profile_id";
alter table "public"."bodyshops" drop constraint "bodyshops_we_profile_id_key";

View File

@@ -0,0 +1,2 @@
alter table "public"."bodyshops" add constraint "bodyshops_we_profile_id_key" unique ("we_profile_id");
alter table "public"."bodyshops" rename column "we_profile_id" to "parts_management_key";

View File

@@ -0,0 +1 @@
alter table "public"."bodyshops" rename column "external_shop_id" to "parts_management_key";

View File

@@ -0,0 +1 @@
alter table "public"."bodyshops" rename column "parts_management_key" to "external_shop_id";

661
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -16,14 +16,14 @@
"job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js"
},
"dependencies": {
"@aws-sdk/client-cloudwatch-logs": "^3.823.0",
"@aws-sdk/client-elasticache": "^3.823.0",
"@aws-sdk/client-s3": "^3.824.0",
"@aws-sdk/client-secrets-manager": "^3.823.0",
"@aws-sdk/client-ses": "^3.823.0",
"@aws-sdk/credential-provider-node": "^3.823.0",
"@aws-sdk/lib-storage": "^3.824.0",
"@aws-sdk/s3-request-presigner": "^3.824.0",
"@aws-sdk/client-cloudwatch-logs": "^3.826.0",
"@aws-sdk/client-elasticache": "^3.826.0",
"@aws-sdk/client-s3": "^3.826.0",
"@aws-sdk/client-secrets-manager": "^3.826.0",
"@aws-sdk/client-ses": "^3.826.0",
"@aws-sdk/credential-provider-node": "^3.826.0",
"@aws-sdk/lib-storage": "^3.826.0",
"@aws-sdk/s3-request-presigner": "^3.826.0",
"@opensearch-project/opensearch": "^2.13.0",
"@socket.io/admin-ui": "^0.5.1",
"@socket.io/redis-adapter": "^8.3.0",
@@ -38,7 +38,7 @@
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"crisp-status-reporter": "^1.2.2",
"dd-trace": "^5.54.0",
"dd-trace": "^5.55.0",
"dinero.js": "^1.9.1",
"dotenv": "^16.4.5",
"express": "^4.21.1",
@@ -81,6 +81,6 @@
"p-limit": "^3.1.0",
"prettier": "^3.5.3",
"supertest": "^7.1.1",
"vitest": "^3.2.2"
"vitest": "^3.2.3"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,257 @@
const crypto = require("crypto");
const admin = require("firebase-admin");
const client = require("../../graphql-client/graphql-client").client;
const DefaultNewShop = require("./defaultNewShop.json");
/**
* Ensures that the required fields are present in the payload.
* @param payload
* @param fields
*/
const requireFields = (payload, fields) => {
for (const field of fields) {
if (!payload[field]) {
throw { status: 400, message: `${field} is required.` };
}
}
};
/**
* Ensures that the email is not already registered in Firebase.
* @param email
* @returns {Promise<void>}
*/
const ensureEmailNotRegistered = async (email) => {
try {
await admin.auth().getUserByEmail(email);
throw { status: 400, message: "userEmail is already registered in Firebase." };
} catch (err) {
if (err.code !== "auth/user-not-found") {
throw { status: 500, message: "Error validating userEmail uniqueness", detail: err };
}
}
};
/**
* Creates a new Firebase user with the provided email.
* @param email
* @returns {Promise<UserRecord>}
*/
const createFirebaseUser = async (email) => {
return admin.auth().createUser({ email });
};
/**
* Deletes a Firebase user by their UID.
* @param uid
* @returns {Promise<void>}
*/
const deleteFirebaseUser = async (uid) => {
return admin.auth().deleteUser(uid);
};
/**
* Generates a password reset link for the given email.
* @param email
* @returns {Promise<string>}
*/
const generateResetLink = async (email) => {
return admin.auth().generatePasswordResetLink(email);
};
/**
* Ensures that the external shop ID is unique in the database.
* @param externalId
* @returns {Promise<void>}
*/
const ensureExternalIdUnique = async (externalId) => {
const query = `
query CHECK_KEY($key: String!) {
bodyshops(where: { external_shop_id: { _eq: $key } }) {
external_shop_id
}
}`;
const resp = await client.request(query, { key: externalId });
if (resp.bodyshops.length) {
throw { status: 400, message: `external_shop_id '${externalId}' is already in use.` };
}
};
/**
* Inserts a new bodyshop into the database.
* @param input
* @returns {Promise<*>}
*/
const insertBodyshop = async (input) => {
const mutation = `
mutation CREATE_SHOP($bs: bodyshops_insert_input!) {
insert_bodyshops_one(object: $bs) { id }
}`;
const resp = await client.request(mutation, { bs: input });
return resp.insert_bodyshops_one.id;
};
/**
* Deletes all vendors associated with a specific shop ID.
* @param shopId
* @returns {Promise<void>}
*/
const deleteVendorsByShop = async (shopId) => {
const mutation = `
mutation DELETE_VENDORS($shopId: uuid!) {
delete_vendors(where: { shopid: { _eq: $shopId } }) {
affected_rows
}
}`;
await client.request(mutation, { shopId });
};
/**
* Deletes a bodyshop by its ID.
* @param shopId
* @returns {Promise<void>}
*/
const deleteBodyshop = async (shopId) => {
const mutation = `
mutation DELETE_SHOP($id: uuid!) {
delete_bodyshops_by_pk(id: $id) { id }
}`;
await client.request(mutation, { id: shopId });
};
/**
* Inserts a new user association into the database.
* @param uid
* @param email
* @param shopId
* @returns {Promise<*>}
*/
const insertUserAssociation = async (uid, email, shopId) => {
const mutation = `
mutation CREATE_USER($u: users_insert_input!) {
insert_users_one(object: $u) {
id: authid
email
}
}`;
const vars = {
u: {
email,
authid: uid,
validemail: true,
associations: {
data: [{ shopid: shopId, authlevel: 80, active: true }]
}
}
};
const resp = await client.request(mutation, vars);
return resp.insert_users_one;
};
/**
* Handles the provisioning of a new parts management shop and user.
* @param req
* @param res
* @returns {Promise<*>}
*/
const partsManagementProvisioning = async (req, res) => {
const { logger } = req;
const p = { ...req.body, userEmail: req.body.userEmail?.toLowerCase() };
try {
// Validate inputs
await ensureEmailNotRegistered(p.userEmail);
requireFields(p, [
"external_shop_id",
"shopname",
"address1",
"city",
"state",
"zip_post",
"country",
"email",
"phone",
"userEmail"
]);
await ensureExternalIdUnique(p.external_shop_id);
logger.log("admin-create-shop-user", "debug", p.userEmail, null, {
request: req.body,
ioadmin: true
});
// Create shop
const shopInput = {
shopname: p.shopname,
address1: p.address1,
address2: p.address2 || null,
city: p.city,
state: p.state,
zip_post: p.zip_post,
country: p.country,
email: p.email,
external_shop_id: p.external_shop_id,
timezone: p.timezone,
phone: p.phone,
logo_img_path: {
src: p.logoUrl,
width: "",
height: "",
headerMargin: DefaultNewShop.logo_img_path.headerMargin
},
md_ro_statuses: DefaultNewShop.md_ro_statuses,
vendors: {
data: p.vendors.map((v) => ({
name: v.name,
street1: v.street1 || null,
street2: v.street2 || null,
city: v.city || null,
state: v.state || null,
zip: v.zip || null,
country: v.country || null,
email: v.email || null,
discount: v.discount ?? 0,
due_date: v.due_date ?? null,
cost_center: v.cost_center || null,
favorite: v.favorite ?? [],
phone: v.phone || null,
active: v.active ?? true,
dmsid: v.dmsid || null
}))
}
};
const newShopId = await insertBodyshop(shopInput);
// Create user + association
const userRecord = await createFirebaseUser(p.userEmail);
const resetLink = await generateResetLink(p.userEmail);
const createdUser = await insertUserAssociation(userRecord.uid, p.userEmail, newShopId);
return res.status(200).json({
shop: { id: newShopId, shopname: p.shopname },
user: {
id: createdUser.id,
email: createdUser.email,
resetLink
}
});
} catch (err) {
logger.log("admin-create-shop-user-error", "error", p.userEmail, null, {
message: err.message,
detail: err.detail || err
});
// Cleanup on failure
if (err.userRecord) {
await deleteFirebaseUser(err.userRecord.uid).catch(() => {});
}
if (err.newShopId) {
await deleteVendorsByShop(err.newShopId).catch(() => {});
await deleteBodyshop(err.newShopId).catch(() => {});
}
return res.status(err.status || 500).json({ error: err.message || "Internal server error" });
}
};
module.exports = partsManagementProvisioning;

View File

@@ -0,0 +1,160 @@
openapi: 3.0.3
info:
title: Parts Management Provisioning API
description: API endpoint to provision a new shop and user in the Parts Management system.
version: 1.0.0
paths:
/parts-management/provision:
post:
summary: Provision a new parts management shop and user
operationId: partsManagementProvisioning
tags:
- Parts Management
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- external_shop_id
- shopname
- address1
- city
- state
- zip_post
- country
- email
- phone
- userEmail
properties:
external_shop_id:
type: string
description: External shop ID (must be unique)
shopname:
type: string
address1:
type: string
address2:
type: string
nullable: true
city:
type: string
state:
type: string
zip_post:
type: string
country:
type: string
email:
type: string
phone:
type: string
userEmail:
type: string
format: email
logoUrl:
type: string
format: uri
nullable: true
timezone:
type: string
nullable: true
vendors:
type: array
items:
type: object
properties:
name:
type: string
street1:
type: string
nullable: true
street2:
type: string
nullable: true
city:
type: string
nullable: true
state:
type: string
nullable: true
zip:
type: string
nullable: true
country:
type: string
nullable: true
email:
type: string
format: email
nullable: true
discount:
type: number
nullable: true
due_date:
type: string
format: date
nullable: true
cost_center:
type: string
nullable: true
favorite:
type: array
items:
type: string
nullable: true
phone:
type: string
nullable: true
active:
type: boolean
nullable: true
dmsid:
type: string
nullable: true
responses:
'200':
description: Shop and user successfully created
content:
application/json:
schema:
type: object
properties:
shop:
type: object
properties:
id:
type: string
format: uuid
shopname:
type: string
user:
type: object
properties:
id:
type: string
email:
type: string
resetLink:
type: string
format: uri
'400':
description: Bad request (missing or invalid fields)
content:
application/json:
schema:
type: object
properties:
error:
type: string
'500':
description: Internal server error
content:
application/json:
schema:
type: object
properties:
error:
type: string

View File

@@ -0,0 +1,23 @@
/**
* Middleware to check if the request is authorized for Parts Management Integration.
* @param req
* @param res
* @param next
* @returns {*}
*/
const partsManagementIntegrationMiddleware = (req, res, next) => {
const secret = process.env.PARTS_MANAGEMENT_INTEGRATION_SECRET;
if (typeof secret !== "string" || secret.length === 0) {
return res.status(500).send("Server misconfiguration");
}
const headerValue = req.headers["parts-management-integration-secret"];
if (typeof headerValue !== "string" || headerValue.trim() !== secret) {
return res.status(401).send("Unauthorized");
}
req.isPartsManagementIntegrationAuthorized = true;
next();
};
module.exports = partsManagementIntegrationMiddleware;

View File

@@ -1,16 +1,19 @@
/**
* VSSTA Integration Middleware
* @param req
* @param res
* @param next
* @returns {*}
* Fails closed if the env var is missing or empty, and strictly compares header.
*/
const vsstaIntegrationMiddleware = (req, res, next) => {
if (req?.headers?.["vssta-integration-secret"] !== process.env?.VSSTA_INTEGRATION_SECRET) {
const secret = process.env.VSSTA_INTEGRATION_SECRET;
if (typeof secret !== "string" || secret.length === 0) {
return res.status(500).send("Server misconfiguration");
}
const headerValue = req.headers["vssta-integration-secret"];
if (typeof headerValue !== "string" || headerValue.trim() !== secret) {
return res.status(401).send("Unauthorized");
}
req.isIntegrationAuthorized = true;
req.isVsstaIntegrationAuthorized = true;
next();
};

View File

@@ -1,8 +1,27 @@
const express = require("express");
const vsstaIntegration = require("../integrations/VSSTA/vsstaIntegrationRoute");
const vsstaMiddleware = require("../middleware/vsstaIntegrationMiddleware");
const router = express.Router();
router.post("/vssta", vsstaMiddleware, vsstaIntegration);
// Pull secrets from env
const { VSSTA_INTEGRATION_SECRET, PARTS_MANAGEMENT_INTEGRATION_SECRET } = process.env;
// Only load VSSTA routes if the secret is set
if (typeof VSSTA_INTEGRATION_SECRET === "string" && VSSTA_INTEGRATION_SECRET.length > 0) {
const vsstaIntegration = require("../integrations/VSSTA/vsstaIntegrationRoute");
const vsstaMiddleware = require("../middleware/vsstaIntegrationMiddleware");
router.post("/vssta", vsstaMiddleware, vsstaIntegration);
} else {
console.warn("VSSTA_INTEGRATION_SECRET is not set — skipping /vssta integration route");
}
// Only load Parts Management routes if that secret is set
if (typeof PARTS_MANAGEMENT_INTEGRATION_SECRET === "string" && PARTS_MANAGEMENT_INTEGRATION_SECRET.length > 0) {
const partsManagementProvisioning = require("../integrations/partsManagement/partsManagementProvisioning");
const partsManagementIntegrationMiddleware = require("../middleware/partsManagementIntegrationMiddleware");
router.post("/parts-management/provision", partsManagementIntegrationMiddleware, partsManagementProvisioning);
} else {
console.warn("PARTS_MANAGEMENT_INTEGRATION_SECRET is not set — skipping /parts-management/provision route");
}
module.exports = router;