Merged in release/2025-12-19 (pull request #2726)
IO-3473 trim user input
This commit is contained in:
@@ -77,9 +77,8 @@ const generateResetLink = async (email) => {
|
|||||||
*/
|
*/
|
||||||
const ensureExternalIdUnique = async (externalId) => {
|
const ensureExternalIdUnique = async (externalId) => {
|
||||||
const resp = await client.request(CHECK_EXTERNAL_SHOP_ID, { key: externalId });
|
const resp = await client.request(CHECK_EXTERNAL_SHOP_ID, { key: externalId });
|
||||||
if (resp.bodyshops.length) {
|
|
||||||
throw { status: 400, message: `external_shop_id '${externalId}' is already in use.` };
|
return !!resp.bodyshops.length;
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -225,10 +224,25 @@ const patchPartsManagementProvisioning = async (req, res) => {
|
|||||||
*/
|
*/
|
||||||
const partsManagementProvisioning = async (req, res) => {
|
const partsManagementProvisioning = async (req, res) => {
|
||||||
const { logger } = req;
|
const { logger } = req;
|
||||||
const body = { ...req.body, userEmail: req.body.userEmail?.toLowerCase() };
|
|
||||||
|
// Trim and normalize email early
|
||||||
|
const body = {
|
||||||
|
...req.body,
|
||||||
|
userEmail: req.body.userEmail?.trim().toLowerCase()
|
||||||
|
};
|
||||||
|
|
||||||
|
const trim = (value) => (typeof value === "string" ? value.trim() : value);
|
||||||
|
const trimIfString = (value) =>
|
||||||
|
value !== null && value !== undefined && typeof value === "string" ? value.trim() : value;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Ensure email is present and trimmed before checking registration
|
||||||
|
if (!body.userEmail) {
|
||||||
|
throw { status: 400, message: "userEmail is required" };
|
||||||
|
}
|
||||||
|
|
||||||
await ensureEmailNotRegistered(body.userEmail);
|
await ensureEmailNotRegistered(body.userEmail);
|
||||||
|
|
||||||
requireFields(body, [
|
requireFields(body, [
|
||||||
"external_shop_id",
|
"external_shop_id",
|
||||||
"shopname",
|
"shopname",
|
||||||
@@ -242,28 +256,68 @@ const partsManagementProvisioning = async (req, res) => {
|
|||||||
"userEmail"
|
"userEmail"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// TODO add in check for early access
|
// Trim all top-level string fields
|
||||||
await ensureExternalIdUnique(body.external_shop_id);
|
const trimmedBody = {
|
||||||
|
...body,
|
||||||
|
external_shop_id: trim(body.external_shop_id),
|
||||||
|
shopname: trim(body.shopname),
|
||||||
|
address1: trim(body.address1),
|
||||||
|
address2: trimIfString(body.address2),
|
||||||
|
city: trim(body.city),
|
||||||
|
state: trim(body.state),
|
||||||
|
zip_post: trim(body.zip_post),
|
||||||
|
country: trim(body.country),
|
||||||
|
email: trim(body.email),
|
||||||
|
phone: trim(body.phone),
|
||||||
|
timezone: trimIfString(body.timezone),
|
||||||
|
logoUrl: trimIfString(body.logoUrl),
|
||||||
|
userPassword: body.userPassword, // passwords should NOT be trimmed (preserves intentional spaces if any, though rare)
|
||||||
|
vendors: Array.isArray(body.vendors)
|
||||||
|
? body.vendors.map((v) => ({
|
||||||
|
name: trim(v.name),
|
||||||
|
street1: trimIfString(v.street1),
|
||||||
|
street2: trimIfString(v.street2),
|
||||||
|
city: trimIfString(v.city),
|
||||||
|
state: trimIfString(v.state),
|
||||||
|
zip: trimIfString(v.zip),
|
||||||
|
country: trimIfString(v.country),
|
||||||
|
email: trimIfString(v.email),
|
||||||
|
cost_center: trimIfString(v.cost_center),
|
||||||
|
phone: trimIfString(v.phone),
|
||||||
|
dmsid: trimIfString(v.dmsid),
|
||||||
|
discount: v.discount ?? 0,
|
||||||
|
due_date: v.due_date ?? null,
|
||||||
|
favorite: v.favorite ?? [],
|
||||||
|
active: v.active ?? true
|
||||||
|
}))
|
||||||
|
: []
|
||||||
|
};
|
||||||
|
|
||||||
logger.log("admin-create-shop-user", "debug", body.userEmail, null, {
|
const duplicateCheck = await ensureExternalIdUnique(trimmedBody.external_shop_id);
|
||||||
|
|
||||||
|
if (duplicateCheck) {
|
||||||
|
throw { status: 400, message: `external_shop_id '${trimmedBody.external_shop_id}' is already in use.` };
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log("admin-create-shop-user", "debug", trimmedBody.userEmail, null, {
|
||||||
request: req.body,
|
request: req.body,
|
||||||
ioadmin: true
|
ioadmin: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const shopInput = {
|
const shopInput = {
|
||||||
shopname: body.shopname,
|
shopname: trimmedBody.shopname,
|
||||||
address1: body.address1,
|
address1: trimmedBody.address1,
|
||||||
address2: body.address2 || null,
|
address2: trimmedBody.address2,
|
||||||
city: body.city,
|
city: trimmedBody.city,
|
||||||
state: body.state,
|
state: trimmedBody.state,
|
||||||
zip_post: body.zip_post,
|
zip_post: trimmedBody.zip_post,
|
||||||
country: body.country,
|
country: trimmedBody.country,
|
||||||
email: body.email,
|
email: trimmedBody.email,
|
||||||
external_shop_id: body.external_shop_id,
|
external_shop_id: trimmedBody.external_shop_id,
|
||||||
timezone: body.timezone || DefaultNewShop.timezone,
|
timezone: trimmedBody.timezone || DefaultNewShop.timezone,
|
||||||
phone: body.phone,
|
phone: trimmedBody.phone,
|
||||||
logo_img_path: {
|
logo_img_path: {
|
||||||
src: body.logoUrl,
|
src: trimmedBody.logoUrl || null, // allow empty logo
|
||||||
width: "",
|
width: "",
|
||||||
height: "",
|
height: "",
|
||||||
headerMargin: DefaultNewShop.logo_img_path.headerMargin
|
headerMargin: DefaultNewShop.logo_img_path.headerMargin
|
||||||
@@ -288,35 +342,37 @@ const partsManagementProvisioning = async (req, res) => {
|
|||||||
appt_alt_transport: DefaultNewShop.appt_alt_transport,
|
appt_alt_transport: DefaultNewShop.appt_alt_transport,
|
||||||
md_jobline_presets: DefaultNewShop.md_jobline_presets,
|
md_jobline_presets: DefaultNewShop.md_jobline_presets,
|
||||||
vendors: {
|
vendors: {
|
||||||
data: body.vendors.map((v) => ({
|
data: trimmedBody.vendors.map((v) => ({
|
||||||
name: v.name,
|
name: v.name,
|
||||||
street1: v.street1 || null,
|
street1: v.street1,
|
||||||
street2: v.street2 || null,
|
street2: v.street2,
|
||||||
city: v.city || null,
|
city: v.city,
|
||||||
state: v.state || null,
|
state: v.state,
|
||||||
zip: v.zip || null,
|
zip: v.zip,
|
||||||
country: v.country || null,
|
country: v.country,
|
||||||
email: v.email || null,
|
email: v.email,
|
||||||
discount: v.discount ?? 0,
|
discount: v.discount,
|
||||||
due_date: v.due_date ?? null,
|
due_date: v.due_date,
|
||||||
cost_center: v.cost_center || null,
|
cost_center: v.cost_center,
|
||||||
favorite: v.favorite ?? [],
|
favorite: v.favorite,
|
||||||
phone: v.phone || null,
|
phone: v.phone,
|
||||||
active: v.active ?? true,
|
active: v.active,
|
||||||
dmsid: v.dmsid || null
|
dmsid: v.dmsid
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const newShopId = await insertBodyshop(shopInput);
|
const newShopId = await insertBodyshop(shopInput);
|
||||||
const userRecord = await createFirebaseUser(body.userEmail, body.userPassword);
|
const userRecord = await createFirebaseUser(trimmedBody.userEmail, trimmedBody.userPassword);
|
||||||
let resetLink = null;
|
let resetLink = null;
|
||||||
if (!body.userPassword) resetLink = await generateResetLink(body.userEmail);
|
if (!trimmedBody.userPassword) {
|
||||||
|
resetLink = await generateResetLink(trimmedBody.userEmail);
|
||||||
|
}
|
||||||
|
|
||||||
const createdUser = await insertUserAssociation(userRecord.uid, body.userEmail, newShopId);
|
const createdUser = await insertUserAssociation(userRecord.uid, trimmedBody.userEmail, newShopId);
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
shop: { id: newShopId, shopname: body.shopname },
|
shop: { id: newShopId, shopname: trimmedBody.shopname },
|
||||||
user: {
|
user: {
|
||||||
id: createdUser.id,
|
id: createdUser.id,
|
||||||
email: createdUser.email,
|
email: createdUser.email,
|
||||||
@@ -324,7 +380,7 @@ const partsManagementProvisioning = async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.log("admin-create-shop-user-error", "error", body.userEmail, null, {
|
logger.log("admin-create-shop-user-error", "error", body.userEmail || "unknown", null, {
|
||||||
message: err.message,
|
message: err.message,
|
||||||
detail: err.detail || err
|
detail: err.detail || err
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user