Merged in release/2025-12-19 (pull request #2726)

IO-3473 trim user input
This commit is contained in:
Dave Richer
2025-12-19 17:10:39 +00:00

View File

@@ -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
}); });