From c7e74d5acd13057210ff447467e9d7d4bcda6a7b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 20:48:49 +0300 Subject: [PATCH 01/14] Update AirQo exceedance production image tag to prod-75b96595-1734025664 --- k8s/exceedance/values-prod-airqo.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/exceedance/values-prod-airqo.yaml b/k8s/exceedance/values-prod-airqo.yaml index 7ad3db6bb6..d1ac25767e 100644 --- a/k8s/exceedance/values-prod-airqo.yaml +++ b/k8s/exceedance/values-prod-airqo.yaml @@ -4,6 +4,6 @@ app: configmap: env-exceedance-production image: repository: eu.gcr.io/airqo-250220/airqo-exceedance-job - tag: prod-48e4e72c-1734023346 + tag: prod-75b96595-1734025664 nameOverride: '' fullnameOverride: '' From ffff52d916f341ef4b7aa906574b74b4b8d9e350 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 20:48:57 +0300 Subject: [PATCH 02/14] Update KCCA exceedance production image tag to prod-75b96595-1734025664 --- k8s/exceedance/values-prod-kcca.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/exceedance/values-prod-kcca.yaml b/k8s/exceedance/values-prod-kcca.yaml index 8a9cbded63..c2bf129f59 100644 --- a/k8s/exceedance/values-prod-kcca.yaml +++ b/k8s/exceedance/values-prod-kcca.yaml @@ -4,6 +4,6 @@ app: configmap: env-exceedance-production image: repository: eu.gcr.io/airqo-250220/kcca-exceedance-job - tag: prod-48e4e72c-1734023346 + tag: prod-75b96595-1734025664 nameOverride: '' fullnameOverride: '' From b88ad7247c547c5d9cd6965b9e13e5e024dcb973 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 20:49:39 +0300 Subject: [PATCH 03/14] Update analytics staging images tag to stage-1396ff4a-1734025613 --- k8s/analytics/values-stage.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/analytics/values-stage.yaml b/k8s/analytics/values-stage.yaml index 114d458f1a..767a845922 100644 --- a/k8s/analytics/values-stage.yaml +++ b/k8s/analytics/values-stage.yaml @@ -8,7 +8,7 @@ images: celeryWorker: eu.gcr.io/airqo-250220/airqo-stage-analytics-celery-worker reportJob: eu.gcr.io/airqo-250220/airqo-stage-analytics-report-job devicesSummaryJob: eu.gcr.io/airqo-250220/airqo-stage-analytics-devices-summary-job - tag: stage-dd764c29-1733849460 + tag: stage-1396ff4a-1734025613 api: name: airqo-stage-analytics-api label: sta-alytics-api From 635707d7cc9f95abbb7fe3df57ee2c1d9da93c71 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 20:49:47 +0300 Subject: [PATCH 04/14] Update website production image tag to prod-75b96595-1734025664 --- k8s/website/values-prod.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/website/values-prod.yaml b/k8s/website/values-prod.yaml index 5fa30b847f..7e02aaefee 100644 --- a/k8s/website/values-prod.yaml +++ b/k8s/website/values-prod.yaml @@ -6,7 +6,7 @@ app: replicaCount: 3 image: repository: eu.gcr.io/airqo-250220/airqo-website-api - tag: prod-48e4e72c-1734023346 + tag: prod-75b96595-1734025664 nameOverride: '' fullnameOverride: '' podAnnotations: {} From a1ab622504063fb3bccfb5e6612fdfb0f3df9784 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 20:49:55 +0300 Subject: [PATCH 05/14] Update device registry production image tag to prod-75b96595-1734025664 --- k8s/device-registry/values-prod.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/device-registry/values-prod.yaml b/k8s/device-registry/values-prod.yaml index d63a9d8eac..579ac310a7 100644 --- a/k8s/device-registry/values-prod.yaml +++ b/k8s/device-registry/values-prod.yaml @@ -6,7 +6,7 @@ app: replicaCount: 3 image: repository: eu.gcr.io/airqo-250220/airqo-device-registry-api - tag: prod-48e4e72c-1734023346 + tag: prod-75b96595-1734025664 nameOverride: '' fullnameOverride: '' podAnnotations: {} From 93370db8fff8f76e157ab1d70e8f19d10b8d0c60 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 20:50:29 +0300 Subject: [PATCH 06/14] Update workflows prod image tag to prod-75b96595-1734025664 --- k8s/workflows/values-prod.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/workflows/values-prod.yaml b/k8s/workflows/values-prod.yaml index d975324084..7470be8ee1 100644 --- a/k8s/workflows/values-prod.yaml +++ b/k8s/workflows/values-prod.yaml @@ -10,7 +10,7 @@ images: initContainer: eu.gcr.io/airqo-250220/airqo-workflows-xcom redisContainer: eu.gcr.io/airqo-250220/airqo-redis containers: eu.gcr.io/airqo-250220/airqo-workflows - tag: prod-48e4e72c-1734023346 + tag: prod-75b96595-1734025664 nameOverride: '' fullnameOverride: '' podAnnotations: {} From cc254d8b7bacc64de70caa1e7b9ed8d6fe9691e5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 20:51:10 +0300 Subject: [PATCH 07/14] Update analytics production image tag to prod-75b96595-1734025664 --- k8s/analytics/values-prod.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/analytics/values-prod.yaml b/k8s/analytics/values-prod.yaml index 95a2d215e8..d280c1d453 100644 --- a/k8s/analytics/values-prod.yaml +++ b/k8s/analytics/values-prod.yaml @@ -8,7 +8,7 @@ images: celeryWorker: eu.gcr.io/airqo-250220/airqo-analytics-celery-worker reportJob: eu.gcr.io/airqo-250220/airqo-analytics-report-job devicesSummaryJob: eu.gcr.io/airqo-250220/airqo-analytics-devices-summary-job - tag: prod-48e4e72c-1734023346 + tag: prod-75b96595-1734025664 api: name: airqo-analytics-api label: analytics-api From 42c8684127669db9c97b72b0b73646e10ff5422d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 20:51:28 +0300 Subject: [PATCH 08/14] Update predict production image tag to prod-75b96595-1734025664 --- k8s/predict/values-prod.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/predict/values-prod.yaml b/k8s/predict/values-prod.yaml index 81a044d332..f0f9eab710 100644 --- a/k8s/predict/values-prod.yaml +++ b/k8s/predict/values-prod.yaml @@ -7,7 +7,7 @@ images: predictJob: eu.gcr.io/airqo-250220/airqo-predict-job trainJob: eu.gcr.io/airqo-250220/airqo-train-job predictPlaces: eu.gcr.io/airqo-250220/airqo-predict-places-air-quality - tag: prod-48e4e72c-1734023346 + tag: prod-75b96595-1734025664 api: name: airqo-prediction-api label: prediction-api From 91eb6fe4d6805fa85ee46272c210fa689d35ff16 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 20:56:24 +0300 Subject: [PATCH 09/14] Update spatial production image tag to prod-75b96595-1734025664 --- k8s/spatial/values-prod.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/spatial/values-prod.yaml b/k8s/spatial/values-prod.yaml index c387dd85dc..0d55fa5a7d 100644 --- a/k8s/spatial/values-prod.yaml +++ b/k8s/spatial/values-prod.yaml @@ -6,7 +6,7 @@ app: replicaCount: 3 image: repository: eu.gcr.io/airqo-250220/airqo-spatial-api - tag: prod-48e4e72c-1734023346 + tag: prod-75b96595-1734025664 nameOverride: '' fullnameOverride: '' podAnnotations: {} From 70fe61e26262c74b63a1b86f570e447bba2f8924 Mon Sep 17 00:00:00 2001 From: baalmart Date: Fri, 13 Dec 2024 10:11:41 +0300 Subject: [PATCH 10/14] ensuring that each preference is unique for each GROUP --- src/auth-service/models/Preference.js | 260 +++++++++++++++++--- src/auth-service/utils/create-preference.js | 94 ++++++- src/auth-service/utils/generate-filter.js | 9 +- 3 files changed, 315 insertions(+), 48 deletions(-) diff --git a/src/auth-service/models/Preference.js b/src/auth-service/models/Preference.js index 9d411f0164..1ddf3a3f3b 100644 --- a/src/auth-service/models/Preference.js +++ b/src/auth-service/models/Preference.js @@ -213,48 +213,228 @@ PreferenceSchema.plugin(uniqueValidator, { message: `{VALUE} should be unique!`, }); -PreferenceSchema.pre("save", function (next) { - const fieldsToUpdate = [ - "selected_sites", - "selected_grids", - "selected_cohorts", - "selected_devices", - "selected_airqlouds", - ]; - - const currentDate = new Date(); - - fieldsToUpdate.forEach((field) => { - if (this[field]) { - this[field] = Array.from( - new Set( - this[field].map((item) => ({ - ...item, - createdAt: item.createdAt || currentDate, - })) - ) +PreferenceSchema.index({ user_id: 1, group_id: 1 }, { unique: true }); + +PreferenceSchema.pre( + [ + "save", + "create", + "update", + "findByIdAndUpdate", + "updateMany", + "updateOne", + "findOneAndUpdate", + ], + async function (next) { + try { + // Determine if this is a new document or an update + const isNew = this.isNew; + const updateData = this.getUpdate ? this.getUpdate() : this; + + // Utility function to validate and process ObjectIds + const processObjectId = (id) => { + return id instanceof mongoose.Types.ObjectId + ? id + : mongoose.Types.ObjectId(id); + }; + + // Comprehensive ID fields processing + const idFieldsToProcess = [ + "airqloud_id", + "airqloud_ids", + "grid_id", + "grid_ids", + "cohort_id", + "cohort_ids", + "network_id", + "network_ids", + "group_id", + "group_ids", + "site_ids", + "device_ids", + ]; + + idFieldsToProcess.forEach((field) => { + if (updateData[field]) { + if (Array.isArray(updateData[field])) { + updateData[field] = updateData[field].map(processObjectId); + } else { + updateData[field] = processObjectId(updateData[field]); + } + } + }); + + // Validate user_id + if (!updateData.user_id) { + return next(new Error("user_id is required")); + } + updateData.user_id = processObjectId(updateData.user_id); + + // Set default values if not provided + const defaultFields = [ + { field: "pollutant", default: "pm2_5" }, + { field: "frequency", default: "hourly" }, + { field: "chartType", default: "line" }, + { field: "chartTitle", default: "Chart Title" }, + { field: "chartSubTitle", default: "Chart SubTitle" }, + ]; + + defaultFields.forEach(({ field, default: defaultValue }) => { + if (isNew && !updateData[field]) { + updateData[field] = defaultValue; + } + }); + + // Handle date fields + if (isNew) { + const currentDate = new Date(); + updateData.startDate = + updateData.startDate || addWeeksToProvideDateTime(currentDate, -2); + updateData.endDate = updateData.endDate || currentDate; + } + + // Validate and process period schema + if (updateData.period) { + const validPeriodFields = ["value", "label", "unitValue", "unit"]; + const periodUpdate = {}; + + validPeriodFields.forEach((field) => { + if (updateData.period[field] !== undefined) { + periodUpdate[field] = updateData.period[field]; + } + }); + + // Additional period validation + if ( + periodUpdate.unitValue !== undefined && + typeof periodUpdate.unitValue !== "number" + ) { + periodUpdate.unitValue = Number(periodUpdate.unitValue); + } + + updateData.period = periodUpdate; + } + + // Process and validate selected arrays with their specific schemas + const selectedArrayProcessors = { + selected_sites: (site) => { + const processedSite = { ...site }; + + // Validate and process ObjectIds + if (site._id) processedSite._id = processObjectId(site._id); + if (site.grid_id) + processedSite.grid_id = processObjectId(site.grid_id); + + // Validate numeric fields + const numericFields = [ + "latitude", + "longitude", + "approximate_latitude", + "approximate_longitude", + ]; + numericFields.forEach((field) => { + if (processedSite[field] !== undefined) { + processedSite[field] = Number(processedSite[field]); + } + }); + + // Ensure createdAt is a valid date + processedSite.createdAt = site.createdAt || new Date(); + + // Validate string fields + const stringFields = [ + "country", + "district", + "sub_county", + "parish", + "county", + "generated_name", + "name", + "city", + "formatted_name", + "region", + "search_name", + ]; + stringFields.forEach((field) => { + if (processedSite[field]) { + processedSite[field] = String(processedSite[field]).trim(); + } + }); + + // Ensure boolean fields + processedSite.isFeatured = !!site.isFeatured; + + return processedSite; + }, + selected_grids: (grid) => ({ + _id: processObjectId(grid._id), + name: String(grid.name).trim(), + createdAt: grid.createdAt || new Date(), + }), + selected_cohorts: (cohort) => ({ + _id: processObjectId(cohort._id), + name: String(cohort.name).trim(), + createdAt: cohort.createdAt || new Date(), + }), + selected_devices: (device) => ({ + _id: processObjectId(device._id), + name: String(device.name).trim(), + createdAt: device.createdAt || new Date(), + }), + selected_airqlouds: (airqloud) => ({ + _id: processObjectId(airqloud._id), + name: String(airqloud.name).trim(), + createdAt: airqloud.createdAt || new Date(), + }), + }; + + // Process selected arrays + Object.keys(selectedArrayProcessors).forEach((field) => { + if (updateData[field]) { + updateData[field] = updateData[field].map( + selectedArrayProcessors[field] + ); + } + }); + + // Prepare $addToSet for array fields to prevent duplicates + const arrayFieldsToAddToSet = [ + ...idFieldsToProcess, + "selected_sites", + "selected_grids", + "selected_devices", + "selected_cohorts", + "selected_airqlouds", + ]; + + arrayFieldsToAddToSet.forEach((field) => { + if (updateData[field]) { + updateData.$addToSet = updateData.$addToSet || {}; + updateData.$addToSet[field] = { + $each: updateData[field], + }; + delete updateData[field]; + } + }); + + // Optional: Add comprehensive logging + console.log( + `Preprocessing preference document: ${isNew ? "New" : "Update"}`, + { + user_id: updateData.user_id, + pollutant: updateData.pollutant, + startDate: updateData.startDate, + endDate: updateData.endDate, + } ); - } - }); - - const fieldsToAddToSet = [ - "airqloud_ids", - "device_ids", - "cohort_ids", - "grid_ids", - "site_ids", - "network_ids", - "group_ids", - ]; - - fieldsToAddToSet.forEach((field) => { - if (this[field]) { - this[field] = Array.from(new Set(this[field].map((id) => id.toString()))); - } - }); - return next(); -}); + next(); + } catch (error) { + console.error("Error in Preference pre-hook:", error); + return next(error); + } + } +); PreferenceSchema.methods = { toJSON() { diff --git a/src/auth-service/utils/create-preference.js b/src/auth-service/utils/create-preference.js index 22b7f72c8a..42c3c31361 100644 --- a/src/auth-service/utils/create-preference.js +++ b/src/auth-service/utils/create-preference.js @@ -44,12 +44,57 @@ const preferences = { const { tenant } = query; logObject("the body", body); const user_id = body.user_id; - const user = await UserModel(tenant).findById(user_id).lean(); - if (isEmpty(user_id) || isEmpty(user)) { - next( - new HttpError("Bad Request Error", httpStatus.BAD_REQUEST, { - message: "The provided User does not exist", - value: user_id, + const group_id = body.group_id; + + if (!isEmpty(user_id)) { + const user = await UserModel(tenant).findById(user_id).lean(); + if (isEmpty(user_id) || isEmpty(user)) { + next( + new HttpError("Bad Request Error", httpStatus.BAD_REQUEST, { + message: "The provided User does not exist", + value: user_id, + }) + ); + } + } + + // Check if user belongs to the specified group + if (!isEmpty(group_id)) { + const userBelongsToGroup = user.group_roles.some( + (role) => role.group.toString() === group_id + ); + + if (!userBelongsToGroup) { + return next( + new HttpError("Bad Request Error", httpStatus.BAD_REQUEST, { + message: "User does not belong to the specified group", + }) + ); + } + } + + const filterResponse = generateFilter.preferences(request, next); + if (isEmpty(filterResponse) || isEmpty(filterResponse.user_id)) { + return { + success: false, + message: "Internal Server Error", + errors: { + message: + "Unable to obtain the corresponding identifier associated with this preference --- please reach out to support@airqo.net", + }, + status: httpStatus.INTERNAL_SERVER_ERROR, + }; + } + + // Check if a preference already exists for this user and group + const existingPreference = await PreferenceModel(tenant).findOne( + filterResponse + ); + + if (existingPreference) { + return next( + new HttpError("Conflict", httpStatus.CONFLICT, { + message: "Preferences for this user and group already exist", }) ); } @@ -138,6 +183,41 @@ const preferences = { body, } = request; + const user_id = body.user_id; + const group_id = body.group_id; + + // Validate user exists and belongs to the group + if (!isEmpty(user_id)) { + const user = await UserModel(tenant).findById(user_id).lean(); + if (isEmpty(user_id) || isEmpty(user)) { + return { + success: false, + message: "Internal Server Error", + errors: { + message: "The provided User does not exist", + }, + status: httpStatus.INTERNAL_SERVER_ERROR, + }; + } + } + + if (!isEmpty(group_id)) { + const userBelongsToGroup = user.group_roles.some( + (role) => role.group.toString() === group_id + ); + + if (!userBelongsToGroup) { + return { + success: false, + message: "Bad Request", + errors: { + message: "User does not belong to the specified group", + }, + status: httpStatus.BAD_REQUEST, + }; + } + } + const fieldsToUpdate = [ "selected_sites", "selected_grids", @@ -170,7 +250,7 @@ const preferences = { }; } - const update = body; + const update = { ...body }; fieldsToAddToSet.forEach((field) => { if (update[field]) { diff --git a/src/auth-service/utils/generate-filter.js b/src/auth-service/utils/generate-filter.js index d0d189152f..549f1e7df4 100644 --- a/src/auth-service/utils/generate-filter.js +++ b/src/auth-service/utils/generate-filter.js @@ -312,15 +312,22 @@ const filter = { }, preferences: (req, next) => { try { - let { user_id } = { + let { user_id, group_id } = { ...req.body, ...req.query, ...req.params, }; + let filter = {}; + if (user_id) { filter["user_id"] = ObjectId(user_id); } + + if (group_id) { + filter["group_id"] = ObjectId(group_id); + } + return filter; } catch (error) { logger.error(`🐛🐛 Internal Server Error ${error.message}`); From faa861bb6f2d6f73edf81a4b3326dbf015381657 Mon Sep 17 00:00:00 2001 From: baalmart Date: Fri, 13 Dec 2024 10:22:39 +0300 Subject: [PATCH 11/14] removing the unique from user_id --- src/auth-service/models/Preference.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/auth-service/models/Preference.js b/src/auth-service/models/Preference.js index 1ddf3a3f3b..78b7894169 100644 --- a/src/auth-service/models/Preference.js +++ b/src/auth-service/models/Preference.js @@ -182,7 +182,6 @@ const PreferenceSchema = new mongoose.Schema( type: ObjectId, required: [true, "user_id is required"], ref: "user", - unique: true, }, site_ids: [ { From 73108c162369d5380306389de0daa2fa284dab6d Mon Sep 17 00:00:00 2001 From: baalmart Date: Fri, 13 Dec 2024 11:54:44 +0300 Subject: [PATCH 12/14] make the code more maintainable --- src/auth-service/utils/create-preference.js | 223 ++++++++++---------- 1 file changed, 106 insertions(+), 117 deletions(-) diff --git a/src/auth-service/utils/create-preference.js b/src/auth-service/utils/create-preference.js index 42c3c31361..e9b623ca63 100644 --- a/src/auth-service/utils/create-preference.js +++ b/src/auth-service/utils/create-preference.js @@ -10,6 +10,72 @@ const isEmpty = require("is-empty"); const logger = log4js.getLogger(`${constants.ENVIRONMENT} -- preferences-util`); const { HttpError } = require("@utils/errors"); +const handleError = (next, title, statusCode, message) => { + next(new HttpError(title, statusCode, { message })); +}; + +const validateUserAndGroup = async (tenant, userId, groupId, next) => { + if (!isEmpty(userId)) { + const user = await UserModel(tenant).findById(userId).lean(); + if (isEmpty(userId) || isEmpty(user)) { + return handleError( + next, + "Bad Request Error", + httpStatus.BAD_REQUEST, + "The provided User does not exist" + ); + } + + if (!isEmpty(groupId)) { + if (user && user.group_roles) { + const userBelongsToGroup = user.group_roles.some( + (role) => role.group.toString() === groupId + ); + if (!userBelongsToGroup) { + return handleError( + next, + "Bad Request Error", + httpStatus.BAD_REQUEST, + "User does not belong to the specified group" + ); + } + } else { + return handleError( + next, + "Bad Request Error", + httpStatus.BAD_REQUEST, + "User not found or invalid user data" + ); + } + } + } +}; + +const prepareUpdate = (body, fieldsToUpdate, fieldsToAddToSet) => { + const update = { ...body }; + + fieldsToAddToSet.forEach((field) => { + if (update[field]) { + update["$addToSet"] = { [field]: { $each: update[field] } }; + delete update[field]; + } + }); + + fieldsToUpdate.forEach((field) => { + if (update[field]) { + update[field] = update[field].map((item) => ({ + ...item, + createdAt: item.createdAt || new Date(), + })); + + update["$addToSet"] = { [field]: { $each: update[field] } }; + delete update[field]; + } + }); + + return update; +}; + const preferences = { list: async (request, next) => { try { @@ -43,59 +109,36 @@ const preferences = { const { body, query } = request; const { tenant } = query; logObject("the body", body); - const user_id = body.user_id; - const group_id = body.group_id; - - if (!isEmpty(user_id)) { - const user = await UserModel(tenant).findById(user_id).lean(); - if (isEmpty(user_id) || isEmpty(user)) { - next( - new HttpError("Bad Request Error", httpStatus.BAD_REQUEST, { - message: "The provided User does not exist", - value: user_id, - }) - ); - } - } - // Check if user belongs to the specified group - if (!isEmpty(group_id)) { - const userBelongsToGroup = user.group_roles.some( - (role) => role.group.toString() === group_id - ); - - if (!userBelongsToGroup) { - return next( - new HttpError("Bad Request Error", httpStatus.BAD_REQUEST, { - message: "User does not belong to the specified group", - }) - ); - } - } + // Validate user and group + const validationError = await validateUserAndGroup( + tenant, + body.user_id, + body.group_id, + next + ); + if (validationError) return; const filterResponse = generateFilter.preferences(request, next); if (isEmpty(filterResponse) || isEmpty(filterResponse.user_id)) { - return { - success: false, - message: "Internal Server Error", - errors: { - message: - "Unable to obtain the corresponding identifier associated with this preference --- please reach out to support@airqo.net", - }, - status: httpStatus.INTERNAL_SERVER_ERROR, - }; + return handleError( + next, + "Internal Server Error", + httpStatus.INTERNAL_SERVER_ERROR, + "Unable to obtain the corresponding identifier associated with this preference --- please reach out to support@airqo.net" + ); } // Check if a preference already exists for this user and group const existingPreference = await PreferenceModel(tenant).findOne( filterResponse ); - if (existingPreference) { - return next( - new HttpError("Conflict", httpStatus.CONFLICT, { - message: "Preferences for this user and group already exist", - }) + return handleError( + next, + "Conflict", + httpStatus.CONFLICT, + "Preferences for this user and group already exist" ); } @@ -183,40 +226,14 @@ const preferences = { body, } = request; - const user_id = body.user_id; - const group_id = body.group_id; - - // Validate user exists and belongs to the group - if (!isEmpty(user_id)) { - const user = await UserModel(tenant).findById(user_id).lean(); - if (isEmpty(user_id) || isEmpty(user)) { - return { - success: false, - message: "Internal Server Error", - errors: { - message: "The provided User does not exist", - }, - status: httpStatus.INTERNAL_SERVER_ERROR, - }; - } - } - - if (!isEmpty(group_id)) { - const userBelongsToGroup = user.group_roles.some( - (role) => role.group.toString() === group_id - ); - - if (!userBelongsToGroup) { - return { - success: false, - message: "Bad Request", - errors: { - message: "User does not belong to the specified group", - }, - status: httpStatus.BAD_REQUEST, - }; - } - } + // Validate user and group + const validationError = await validateUserAndGroup( + tenant, + body.user_id, + body.group_id, + next + ); + if (validationError) return; const fieldsToUpdate = [ "selected_sites", @@ -237,45 +254,18 @@ const preferences = { ]; const filterResponse = generateFilter.preferences(request, next); - if (isEmpty(filterResponse) || isEmpty(filterResponse.user_id)) { - return { - success: false, - message: "Internal Server Error", - errors: { - message: - "Unable to obtain the corresponding identifier associated with this preference --- please reach out to support@airqo.net", - }, - status: httpStatus.INTERNAL_SERVER_ERROR, - }; + return handleError( + next, + "Internal Server Error", + httpStatus.INTERNAL_SERVER_ERROR, + "Unable to obtain the corresponding identifier associated with this preference --- please reach out to support@airqo.net" + ); } - const update = { ...body }; - - fieldsToAddToSet.forEach((field) => { - if (update[field]) { - update["$addToSet"] = { - [field]: { $each: update[field] }, - }; - delete update[field]; - } - }); - - fieldsToUpdate.forEach((field) => { - if (update[field]) { - update[field] = update[field].map((item) => ({ - ...item, - createdAt: item.createdAt || new Date(), - })); - update["$addToSet"] = { - [field]: { $each: update[field] }, - }; - delete update[field]; - } - }); + const update = prepareUpdate(body, fieldsToUpdate, fieldsToAddToSet); const options = { upsert: true, new: true }; - const modifyResponse = await PreferenceModel(tenant).findOneAndUpdate( filterResponse, update, @@ -285,17 +275,16 @@ const preferences = { if (!isEmpty(modifyResponse)) { return { success: true, - message: "successfully created or updated a preference", + message: "Successfully created or updated a preference", data: modifyResponse, status: httpStatus.OK, }; } else { - next( - new HttpError( - "Internal Server Error", - httpStatus.INTERNAL_SERVER_ERROR, - { message: "unable to create or update a preference" } - ) + return handleError( + next, + "Internal Server Error", + httpStatus.INTERNAL_SERVER_ERROR, + "Unable to create or update a preference" ); } } catch (error) { From 00a93ae1385f3a565a53a5af791611a5f4e4e4db Mon Sep 17 00:00:00 2001 From: baalmart Date: Fri, 13 Dec 2024 12:00:29 +0300 Subject: [PATCH 13/14] processObjectId function could be more robust by adding type checking and error handling --- src/auth-service/models/Preference.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/auth-service/models/Preference.js b/src/auth-service/models/Preference.js index 78b7894169..8ee2d51161 100644 --- a/src/auth-service/models/Preference.js +++ b/src/auth-service/models/Preference.js @@ -232,9 +232,14 @@ PreferenceSchema.pre( // Utility function to validate and process ObjectIds const processObjectId = (id) => { - return id instanceof mongoose.Types.ObjectId - ? id - : mongoose.Types.ObjectId(id); + if (!id) return null; + if (id instanceof mongoose.Types.ObjectId) return id; + try { + return mongoose.Types.ObjectId(id); + } catch (error) { + logger.error(`Invalid ObjectId: ${id}`); + throw new Error(`Invalid ObjectId: ${id}`); + } }; // Comprehensive ID fields processing From 6789bad066cccc07d48cf42e511c2c1259e9f347 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 13 Dec 2024 12:08:34 +0300 Subject: [PATCH 14/14] Update auth service staging image tag to stage-c701f88b-1734080836 --- k8s/auth-service/values-stage.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/auth-service/values-stage.yaml b/k8s/auth-service/values-stage.yaml index 9c424ce1a5..32f9f2d950 100644 --- a/k8s/auth-service/values-stage.yaml +++ b/k8s/auth-service/values-stage.yaml @@ -6,7 +6,7 @@ app: replicaCount: 2 image: repository: eu.gcr.io/airqo-250220/airqo-stage-auth-api - tag: stage-95dea6dd-1733435752 + tag: stage-c701f88b-1734080836 nameOverride: '' fullnameOverride: '' podAnnotations: {}