Merged in feature/IO-3255-simplified-part-management (pull request #2534)
feature/IO-3255-simplified-parts-management - Fix deprovision route
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
const admin = require("firebase-admin");
|
const admin = require("firebase-admin");
|
||||||
const client = require("../../../graphql-client/graphql-client").client;
|
const client = require("../../../graphql-client/graphql-client").client;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
DELETE_SHOP,
|
DELETE_SHOP,
|
||||||
DELETE_VENDORS_BY_SHOP,
|
DELETE_VENDORS_BY_SHOP,
|
||||||
@@ -18,145 +17,154 @@ const {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a Firebase user by UID.
|
* Deletes a Firebase user by UID.
|
||||||
* @param uid
|
* @param {string} uid - The Firebase user ID
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
const deleteFirebaseUser = async (uid) => {
|
const deleteFirebaseUser = async (uid) => {
|
||||||
|
if (!uid) throw new Error("User UID is required");
|
||||||
return admin.auth().deleteUser(uid);
|
return admin.auth().deleteUser(uid);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes all vendors associated with a shop.
|
* Deletes all vendors associated with a shop.
|
||||||
* @param shopId
|
* @param {string} shopId - The shop ID
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
const deleteVendorsByShop = async (shopId) => {
|
const deleteVendorsByShop = async (shopId) => {
|
||||||
|
if (!shopId) throw new Error("Shop ID is required");
|
||||||
await client.request(DELETE_VENDORS_BY_SHOP, { shopId });
|
await client.request(DELETE_VENDORS_BY_SHOP, { shopId });
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a bodyshop from the database.
|
* Deletes a bodyshop from the database.
|
||||||
* @param shopId
|
* @param {string} shopId - The shop ID
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
const deleteBodyshop = async (shopId) => {
|
const deleteBodyshop = async (shopId) => {
|
||||||
|
if (!shopId) throw new Error("Shop ID is required");
|
||||||
await client.request(DELETE_SHOP, { id: shopId });
|
await client.request(DELETE_SHOP, { id: shopId });
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch job ids for a given shop
|
* Fetch job ids for a given shop
|
||||||
* @param shopId
|
* @param {string} shopId - The shop ID
|
||||||
* @returns {Promise<string[]>}
|
* @returns {Promise<string[]>}
|
||||||
*/
|
*/
|
||||||
const getJobIdsForShop = async (shopId) => {
|
const getJobIdsForShop = async (shopId) => {
|
||||||
|
if (!shopId) throw new Error("Shop ID is required");
|
||||||
const resp = await client.request(GET_JOBS_BY_SHOP, { shopId });
|
const resp = await client.request(GET_JOBS_BY_SHOP, { shopId });
|
||||||
return resp.jobs.map((j) => j.id);
|
return resp.jobs?.map((j) => j.id) || [];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete joblines for the given job ids
|
* Delete joblines for the given job ids
|
||||||
* @param jobIds {string[]}
|
* @param {string[]} jobIds - Array of job IDs
|
||||||
* @returns {Promise<number>} affected rows
|
* @returns {Promise<number>} affected rows
|
||||||
*/
|
*/
|
||||||
const deleteJoblinesForJobs = async (jobIds) => {
|
const deleteJoblinesForJobs = async (jobIds) => {
|
||||||
if (!jobIds.length) return 0;
|
if (!jobIds?.length) return 0;
|
||||||
const resp = await client.request(DELETE_JOBLINES_BY_JOB_IDS, { jobIds });
|
const resp = await client.request(DELETE_JOBLINES_BY_JOB_IDS, { jobIds });
|
||||||
return resp.delete_joblines.affected_rows;
|
return resp.delete_joblines?.affected_rows || 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete jobs for the given job ids
|
* Delete jobs for the given job ids
|
||||||
* @param jobIds {string[]}
|
* @param {string[]} jobIds - Array of job IDs
|
||||||
* @returns {Promise<number>} affected rows
|
* @returns {Promise<number>} affected rows
|
||||||
*/
|
*/
|
||||||
const deleteJobsByIds = async (jobIds) => {
|
const deleteJobsByIds = async (jobIds) => {
|
||||||
if (!jobIds.length) return 0;
|
if (!jobIds?.length) return 0;
|
||||||
const resp = await client.request(DELETE_JOBS_BY_IDS, { jobIds });
|
const resp = await client.request(DELETE_JOBS_BY_IDS, { jobIds });
|
||||||
return resp.delete_jobs.affected_rows;
|
return resp.delete_jobs?.affected_rows || 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles deprovisioning a shop for parts management.
|
* Handles deprovisioning a shop for parts management.
|
||||||
* @param req
|
* @param {Object} req - Express request object
|
||||||
* @param res
|
* @param {Object} res - Express response object
|
||||||
* @returns {Promise<*>}
|
* @returns {Promise<*>}
|
||||||
*/
|
*/
|
||||||
const partsManagementDeprovisioning = async (req, res) => {
|
const partsManagementDeprovisioning = async (req, res) => {
|
||||||
const { logger } = req;
|
const { logger } = req;
|
||||||
const body = req.body;
|
const { shopId } = req.body;
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "production") {
|
if (process.env.NODE_ENV === "production") {
|
||||||
return res.status(403).json({ error: "Deprovisioning not allowed in production environment." });
|
return res.status(403).json({ error: "Deprovisioning not allowed in production environment." });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!body.shopId) {
|
if (!shopId) {
|
||||||
throw { status: 400, message: "shopId is required." };
|
throw { status: 400, message: "shopId is required." };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch bodyshop and check external_shop_id
|
// Fetch bodyshop and check external_shop_id
|
||||||
const shopResp = await client.request(GET_BODYSHOP, { id: body.shopId });
|
const shopResp = await client.request(GET_BODYSHOP, { id: shopId });
|
||||||
const shop = shopResp.bodyshops_by_pk;
|
const shop = shopResp.bodyshops_by_pk;
|
||||||
if (!shop) {
|
if (!shop) {
|
||||||
throw { status: 404, message: `Bodyshop with id ${body.shopId} not found.` };
|
throw { status: 404, message: `Bodyshop with id ${shopId} not found.` };
|
||||||
}
|
}
|
||||||
if (!shop.external_shop_id) {
|
if (!shop.external_shop_id) {
|
||||||
throw { status: 400, message: "Cannot delete bodyshop without external_shop_id." };
|
throw { status: 400, message: "Cannot delete bodyshop without external_shop_id." };
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log("admin-delete-shop", "debug", null, null, {
|
logger.log("admin-delete-shop", "debug", null, null, {
|
||||||
shopId: body.shopId,
|
shopId,
|
||||||
shopname: shop.shopname,
|
shopname: shop.shopname,
|
||||||
ioadmin: true
|
ioadmin: true
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get vendors
|
// Get vendors
|
||||||
const vendorsResp = await client.request(GET_VENDORS, { shopId: body.shopId });
|
const vendorsResp = await client.request(GET_VENDORS, { shopId });
|
||||||
const deletedVendors = vendorsResp.vendors.map((v) => v.name);
|
const deletedVendors = vendorsResp.vendors?.map((v) => v.name) || [];
|
||||||
|
|
||||||
// Get associated users
|
// Get associated users
|
||||||
const assocResp = await client.request(GET_ASSOCIATED_USERS, { shopId: body.shopId });
|
const assocResp = await client.request(GET_ASSOCIATED_USERS, { shopId });
|
||||||
const associatedUsers = assocResp.associations.map((assoc) => ({
|
const associatedUsers =
|
||||||
authId: assoc.user.authid,
|
assocResp.associations?.map((assoc) => ({
|
||||||
email: assoc.user.email
|
authId: assoc.user?.authid,
|
||||||
}));
|
email: assoc.user?.email
|
||||||
|
})) || [];
|
||||||
|
|
||||||
// Delete associations for the shop
|
// Delete associations for the shop
|
||||||
const assocDeleteResp = await client.request(DELETE_ASSOCIATIONS_BY_SHOP, { shopId: body.shopId });
|
const assocDeleteResp = await client.request(DELETE_ASSOCIATIONS_BY_SHOP, { shopId });
|
||||||
const associationsDeleted = assocDeleteResp.delete_associations.affected_rows;
|
const associationsDeleted = assocDeleteResp.delete_associations?.affected_rows || 0;
|
||||||
|
|
||||||
// For each user, check if they have remaining associations; if not, delete user and Firebase account
|
// Delete users with no remaining associations
|
||||||
const deletedUsers = [];
|
const deletedUsers = [];
|
||||||
for (const user of associatedUsers) {
|
for (const user of associatedUsers) {
|
||||||
const countResp = await client.request(GET_USER_ASSOCIATIONS_COUNT, { userEmail: user.email });
|
if (!user.email || !user.authId) continue;
|
||||||
// Determine which users now have zero associations and should be deleted (defer deletion until end)
|
try {
|
||||||
const emailsToAuthId = associatedUsers.reduce((acc, u) => {
|
const countResp = await client.request(GET_USER_ASSOCIATIONS_COUNT, { userEmail: user.email });
|
||||||
acc[u.email] = u.authId;
|
const assocCount = countResp.associations_aggregate?.aggregate?.count || 0;
|
||||||
return acc;
|
if (assocCount === 0) {
|
||||||
}, {});
|
await client.request(DELETE_USER, { email: user.email });
|
||||||
const emailsToDelete = [];
|
await deleteFirebaseUser(user.authId);
|
||||||
await client.request(DELETE_USER, { email: user.email });
|
deletedUsers.push(user.email);
|
||||||
await deleteFirebaseUser(user.authId);
|
}
|
||||||
deletedUsers.push(user.email);
|
} catch (userError) {
|
||||||
|
logger.log("admin-delete-user-error", "warn", null, null, {
|
||||||
|
email: user.email,
|
||||||
|
error: userError.message
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
emailsToDelete.push(user.email);
|
|
||||||
const jobIds = await getJobIdsForShop(body.shopId);
|
// Delete jobs and joblines
|
||||||
|
const jobIds = await getJobIdsForShop(shopId);
|
||||||
const joblinesDeleted = await deleteJoblinesForJobs(jobIds);
|
const joblinesDeleted = await deleteJoblinesForJobs(jobIds);
|
||||||
const jobsDeleted = await deleteJobsByIds(jobIds);
|
const jobsDeleted = await deleteJobsByIds(jobIds);
|
||||||
|
|
||||||
// Delete any audit trail entries tied to this bodyshop to avoid FK violations
|
// Delete audit trail
|
||||||
const auditResp = await client.request(DELETE_AUDIT_TRAIL_BY_SHOP, { shopId: body.shopId });
|
const auditResp = await client.request(DELETE_AUDIT_TRAIL_BY_SHOP, { shopId });
|
||||||
const auditDeleted = auditResp.delete_audit_trail.affected_rows;
|
const auditDeleted = auditResp.delete_audit_trail?.affected_rows || 0;
|
||||||
|
|
||||||
// Delete vendors
|
// Delete vendors and shop
|
||||||
await deleteVendorsByShop(body.shopId);
|
await deleteVendorsByShop(shopId);
|
||||||
|
await deleteBodyshop(shopId);
|
||||||
// Delete shop
|
|
||||||
await deleteBodyshop(body.shopId);
|
|
||||||
|
|
||||||
// Summary log
|
// Summary log
|
||||||
logger.log("admin-delete-shop-summary", "info", null, null, {
|
logger.log("admin-delete-shop-summary", "info", null, null, {
|
||||||
shopId: body.shopId,
|
shopId,
|
||||||
shopname: shop.shopname,
|
shopname: shop.shopname,
|
||||||
associationsDeleted,
|
associationsDeleted,
|
||||||
deletedUsers,
|
deletedUsers,
|
||||||
@@ -167,31 +175,20 @@ const partsManagementDeprovisioning = async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
message: `Bodyshop ${body.shopId} and associated resources deleted successfully.`,
|
message: `Bodyshop ${shopId} and associated resources deleted successfully.`,
|
||||||
deletedShop: { id: body.shopId, name: shop.shopname },
|
deletedShop: { id: shopId, name: shop.shopname },
|
||||||
deletedAssociationsCount: associationsDeleted,
|
deletedAssociationsCount: associationsDeleted,
|
||||||
deletedUsers: deletedUsers,
|
deletedUsers,
|
||||||
deletedVendors: deletedVendors,
|
deletedVendors,
|
||||||
deletedJoblinesCount: joblinesDeleted,
|
deletedJoblinesCount: joblinesDeleted,
|
||||||
deletedJobsCount: jobsDeleted,
|
deletedJobsCount: jobsDeleted,
|
||||||
deletedAuditTrailCount: auditDeleted
|
deletedAuditTrailCount: auditDeleted
|
||||||
});
|
});
|
||||||
// Now delete users that have no remaining associations and their Firebase accounts
|
|
||||||
const deletedUsers = [];
|
|
||||||
for (const email of emailsToDelete) {
|
|
||||||
await client.request(DELETE_USER, { email });
|
|
||||||
const authId = emailsToAuthId[email];
|
|
||||||
if (authId) {
|
|
||||||
await deleteFirebaseUser(authId);
|
|
||||||
}
|
|
||||||
deletedUsers.push(email);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.log("admin-delete-shop-error", "error", null, null, {
|
logger.log("admin-delete-shop-error", "error", null, null, {
|
||||||
message: err.message,
|
message: err.message,
|
||||||
detail: err.detail || err
|
detail: err.detail || err.stack || err
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(err.status || 500).json({ error: err.message || "Internal server error" });
|
return res.status(err.status || 500).json({ error: err.message || "Internal server error" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user