feature/IO-3258-Shop-User-Vendor-Creation: Finish
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user