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} */ 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} */ const createFirebaseUser = async (email) => { return admin.auth().createUser({ email }); }; /** * Deletes a Firebase user by their UID. * @param uid * @returns {Promise} */ const deleteFirebaseUser = async (uid) => { return admin.auth().deleteUser(uid); }; /** * Generates a password reset link for the given email. * @param email * @returns {Promise} */ const generateResetLink = async (email) => { return admin.auth().generatePasswordResetLink(email); }; /** * Ensures that the external shop ID is unique in the database. * @param externalId * @returns {Promise} */ 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} */ 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} */ 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(() => { // Ignore errors during user deletion cleanup }); } if (err.newShopId) { await deleteVendorsByShop(err.newShopId).catch(() => { // Ignore errors during vendor deletion cleanup }); await deleteBodyshop(err.newShopId).catch(() => { // Ignore errors during shop deletion cleanup }); } return res.status(err.status || 500).json({ error: err.message || "Internal server error" }); } }; module.exports = partsManagementProvisioning;