diff --git a/k8s/analytics/values-prod.yaml b/k8s/analytics/values-prod.yaml index 5b2d75efa6..edf59231b3 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-ffa7c76b-1732557102 + tag: prod-8a436c69-1732561912 api: name: airqo-analytics-api label: analytics-api diff --git a/k8s/auth-service/values-prod.yaml b/k8s/auth-service/values-prod.yaml index 2543fdc24a..2f9a58b030 100644 --- a/k8s/auth-service/values-prod.yaml +++ b/k8s/auth-service/values-prod.yaml @@ -6,7 +6,7 @@ app: replicaCount: 3 image: repository: eu.gcr.io/airqo-250220/airqo-auth-api - tag: prod-ffa7c76b-1732557102 + tag: prod-8a436c69-1732561912 nameOverride: '' fullnameOverride: '' podAnnotations: {} diff --git a/k8s/auth-service/values-stage.yaml b/k8s/auth-service/values-stage.yaml index bd0736700e..83bd931089 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-bd23841e-1732556820 + tag: stage-75cbc431-1732565904 nameOverride: '' fullnameOverride: '' podAnnotations: {} diff --git a/k8s/device-registry/values-prod.yaml b/k8s/device-registry/values-prod.yaml index c2659865aa..4ae4c24318 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-ffa7c76b-1732557102 + tag: prod-8a436c69-1732561912 nameOverride: '' fullnameOverride: '' podAnnotations: {} diff --git a/k8s/exceedance/values-prod-airqo.yaml b/k8s/exceedance/values-prod-airqo.yaml index 418ff5d8df..fbee287b9e 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-ffa7c76b-1732557102 + tag: prod-8a436c69-1732561912 nameOverride: '' fullnameOverride: '' diff --git a/k8s/exceedance/values-prod-kcca.yaml b/k8s/exceedance/values-prod-kcca.yaml index f041699e1b..02442487d8 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-ffa7c76b-1732557102 + tag: prod-8a436c69-1732561912 nameOverride: '' fullnameOverride: '' diff --git a/k8s/predict/values-prod.yaml b/k8s/predict/values-prod.yaml index faffa5d35b..bde482df8d 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-ffa7c76b-1732557102 + tag: prod-8a436c69-1732561912 api: name: airqo-prediction-api label: prediction-api diff --git a/src/auth-service/models/User.js b/src/auth-service/models/User.js index 91def111b6..14d6c5de9f 100644 --- a/src/auth-service/models/User.js +++ b/src/auth-service/models/User.js @@ -17,7 +17,6 @@ const logger = require("log4js").getLogger( const validUserTypes = ["user", "guest"]; const { HttpError } = require("@utils/errors"); const mailer = require("@utils/mailer"); -const ORGANISATIONS_LIMIT = 6; function oneMonthFromNow() { var d = new Date(); @@ -248,27 +247,14 @@ UserSchema.path("group_roles.userType").validate(function (value) { return validUserTypes.includes(value); }, "Invalid userType value"); -UserSchema.pre("save", async function (next) { - // Password hashing +UserSchema.pre("save", function (next) { if (this.isModified("password")) { this.password = bcrypt.hashSync(this.password, saltRounds); } - - // Validate contact information if (!this.email && !this.phoneNumber) { return next(new Error("Phone number or email is required!")); } - // Profile picture validation - if (this.profilePicture && !validateProfilePicture(this.profilePicture)) { - return next( - new HttpError("Bad Request Error", httpStatus.BAD_REQUEST, { - message: "Invalid profile picture URL", - }) - ); - } - - // Network roles handling if (!this.network_roles || this.network_roles.length === 0) { if ( !constants || @@ -295,7 +281,6 @@ UserSchema.pre("save", async function (next) { ]; } - // Group roles handling if (!this.group_roles || this.group_roles.length === 0) { if ( !constants || @@ -307,7 +292,7 @@ UserSchema.pre("save", async function (next) { httpStatus.INTERNAL_SERVER_ERROR, { message: - "Contact support@airqo.net -- unable to retrieve the default Group or Role", + "Contact support@airqo.net -- unable to retrieve the default Group or Role to which the User will belong", } ); } @@ -322,119 +307,23 @@ UserSchema.pre("save", async function (next) { ]; } - // Permissions handling - if (this.permissions && this.permissions.length > 0) { - // Additional permissions validation can be added here if needed - this.permissions = [...new Set(this.permissions)]; // Ensure unique permissions + if (!this.verified) { + this.verified = false; } - // Ensure default values - this.verified = this.verified ?? false; - this.analyticsVersion = this.analyticsVersion ?? 2; + if (!this.analyticsVersion) { + this.analyticsVersion = 2; + } return next(); }); -UserSchema.pre( - ["updateOne", "findOneAndUpdate", "updateMany", "update", "save"], - function (next) { - if (this.getUpdate) { - const updates = this.getUpdate(); - const fieldsToValidate = [ - "_id", - "firstName", - "lastName", - "userName", - "email", - "organization", - "long_organization", - "privilege", - "country", - "profilePicture", - "phoneNumber", - "createdAt", - "updatedAt", - "rateLimit", - "lastLogin", - "iat", - ]; - - fieldsToValidate.forEach((field) => { - // Check for empty/null/undefined values - const value = updates[field] || (updates.$set && updates.$set[field]); - - if (value === undefined || value === null || value === "") { - return next( - new HttpError("Validation Error", httpStatus.BAD_REQUEST, { - message: `${field} cannot be empty, null, or undefined`, - }) - ); - } - }); - if (updates) { - // Prevent modification of certain immutable fields - const immutableFields = ["firebase_uid", "email", "createdAt", "_id"]; - - immutableFields.forEach((field) => { - if (updates[field]) delete updates[field]; - - if (updates.$set && updates.$set[field]) { - return next( - new HttpError( - "Modification Not Allowed", - httpStatus.BAD_REQUEST, - { message: `Cannot modify ${field} after creation` } - ) - ); - } - - if (updates.$set) delete updates.$set[field]; - if (updates.$push) delete updates.$push[field]; - }); - - // Validate network roles and group roles limits - if ( - updates.network_roles && - updates.network_roles.length > ORGANISATIONS_LIMIT - ) { - return next( - new HttpError("Validation Error", httpStatus.BAD_REQUEST, { - message: `Maximum ${ORGANISATIONS_LIMIT} network roles allowed`, - }) - ); - } - - if ( - updates.group_roles && - updates.group_roles.length > ORGANISATIONS_LIMIT - ) { - return next( - new HttpError("Validation Error", httpStatus.BAD_REQUEST, { - message: `Maximum ${ORGANISATIONS_LIMIT} group roles allowed`, - }) - ); - } - - // Ensure password is hashed if modified - if (updates.password) { - updates.password = bcrypt.hashSync(updates.password, saltRounds); - } - } - } - - // Additional checks for new documents - if (this.isNew) { - const requiredFields = ["firstName", "lastName", "email"]; - requiredFields.forEach((field) => { - if (this.isModified(field) && !this[field]) { - return next(new Error(`${field} is required`)); - } - }); - } - - next(); +UserSchema.pre("update", function (next) { + if (this.isModified("password")) { + this.password = bcrypt.hashSync(this.password, saltRounds); } -); + return next(); +}); UserSchema.index({ email: 1 }, { unique: true }); UserSchema.index({ userName: 1 }, { unique: true }); @@ -813,18 +702,61 @@ UserSchema.statics = { async modify({ filter = {}, update = {} } = {}, next) { try { logText("the user modification function........"); - const options = { new: true }; + let options = { new: true }; const fieldNames = Object.keys(update); const fieldsString = fieldNames.join(" "); + let modifiedUpdate = update; + modifiedUpdate["$addToSet"] = {}; + + if (update.password) { + modifiedUpdate.password = bcrypt.hashSync(update.password, saltRounds); + } + + if (modifiedUpdate.profilePicture) { + if (!validateProfilePicture(modifiedUpdate.profilePicture)) { + next( + new HttpError("Bad Request Error", httpStatus.BAD_REQUEST, { + message: "Invalid profile picture URL", + }) + ); + } + } + + if (modifiedUpdate.network_roles) { + if (isEmpty(modifiedUpdate.network_roles.network)) { + delete modifiedUpdate.network_roles; + } else { + modifiedUpdate["$addToSet"] = { + network_roles: { $each: modifiedUpdate.network_roles }, + }; + delete modifiedUpdate.network_roles; + } + } + + if (modifiedUpdate.group_roles) { + if (isEmpty(modifiedUpdate.group_roles.group)) { + delete modifiedUpdate.group_roles; + } else { + modifiedUpdate["$addToSet"] = { + group_roles: { $each: modifiedUpdate.group_roles }, + }; + delete modifiedUpdate.group_roles; + } + } + + if (modifiedUpdate.permissions) { + modifiedUpdate["$addToSet"]["permissions"] = {}; + modifiedUpdate["$addToSet"]["permissions"]["$each"] = + modifiedUpdate.permissions; + delete modifiedUpdate["permissions"]; + } - // Find and update user const updatedUser = await this.findOneAndUpdate( filter, - update, + modifiedUpdate, options ).select(fieldsString); - // Handle update result if (!isEmpty(updatedUser)) { const { _id, ...userData } = updatedUser._doc; return { @@ -833,17 +765,16 @@ UserSchema.statics = { data: userData, status: httpStatus.OK, }; + } else if (isEmpty(updatedUser)) { + next( + new HttpError("Bad Request Error", httpStatus.BAD_REQUEST, { + message: "user does not exist, please crosscheck", + }) + ); } - - // User not found - return next( - new HttpError("Bad Request Error", httpStatus.BAD_REQUEST, { - message: "user does not exist, please crosscheck", - }) - ); } catch (error) { logger.error(`🐛🐛 Internal Server Error -- ${error.message}`); - return next( + next( new HttpError( "Internal Server Error", httpStatus.INTERNAL_SERVER_ERROR, diff --git a/src/auth-service/models/UserNew.js b/src/auth-service/models/UserNew.js new file mode 100644 index 0000000000..91def111b6 --- /dev/null +++ b/src/auth-service/models/UserNew.js @@ -0,0 +1,1006 @@ +const mongoose = require("mongoose").set("debug", true); +const Schema = mongoose.Schema; +const validator = require("validator"); +const bcrypt = require("bcrypt"); +const jwt = require("jsonwebtoken"); +const constants = require("@config/constants"); +const { logObject, logText } = require("@utils/log"); +const ObjectId = mongoose.Schema.Types.ObjectId; +const isEmpty = require("is-empty"); +const saltRounds = constants.SALT_ROUNDS; +const httpStatus = require("http-status"); +const accessCodeGenerator = require("generate-password"); +const { getModelByTenant } = require("@config/database"); +const logger = require("log4js").getLogger( + `${constants.ENVIRONMENT} -- user-model` +); +const validUserTypes = ["user", "guest"]; +const { HttpError } = require("@utils/errors"); +const mailer = require("@utils/mailer"); +const ORGANISATIONS_LIMIT = 6; + +function oneMonthFromNow() { + var d = new Date(); + var targetMonth = d.getMonth() + 1; + d.setMonth(targetMonth); + if (d.getMonth() !== targetMonth % 12) { + d.setDate(0); // last day of previous month + } + return d; +} + +function validateProfilePicture(profilePicture) { + const urlRegex = + /^(http(s)?:\/\/.)[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/g; + if (!urlRegex.test(profilePicture)) { + logger.error(`🙅🙅 Bad Request Error -- Not a valid profile picture URL`); + return false; + } + if (profilePicture.length > 200) { + logText("longer than 200 chars"); + logger.error( + `🙅🙅 Bad Request Error -- profile picture URL exceeds 200 characters` + ); + return false; + } + return true; +} +const passwordReg = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d@#?!$%^&*,.]{6,}$/; +const UserSchema = new Schema( + { + due_date: { type: Date }, + status: { type: String }, + address: { type: String }, + country: { type: String }, + firebase_uid: { type: String }, + city: { type: String }, + department_id: { + type: ObjectId, + ref: "department", + }, + role: { + type: ObjectId, + ref: "role", + }, + birthday: { type: Date }, + reports_to: { type: ObjectId, ref: "user" }, + replaced_by: { type: ObjectId, ref: "user" }, + email: { + type: String, + unique: true, + required: [true, "Email is required"], + trim: true, + validate: { + validator(email) { + return validator.isEmail(email); + }, + message: "{VALUE} is not a valid email!", + }, + }, + verified: { + type: Boolean, + default: false, + }, + analyticsVersion: { type: Number, default: 2 }, + firstName: { + type: String, + required: [true, "FirstName is required!"], + trim: true, + }, + lastName: { + type: String, + required: [true, "LastName is required"], + trim: true, + }, + userName: { + type: String, + required: [true, "UserName is required!"], + trim: true, + unique: true, + }, + password: { + type: String, + required: [true, "Password is required!"], + trim: true, + minlength: [6, "Password is required"], + validate: { + validator(password) { + return passwordReg.test(password); + }, + message: "{VALUE} is not a valid password, please check documentation!", + }, + }, + privilege: { + type: String, + default: "user", + }, + isActive: { type: Boolean, default: false }, + loginCount: { type: Number, default: 0 }, + duration: { type: Date, default: oneMonthFromNow }, + network_roles: { + type: [ + { + network: { + type: ObjectId, + ref: "network", + default: mongoose.Types.ObjectId(constants.DEFAULT_NETWORK), + }, + userType: { type: String, default: "guest", enum: validUserTypes }, + createdAt: { type: String, default: new Date() }, + role: { + type: ObjectId, + ref: "role", + default: mongoose.Types.ObjectId(constants.DEFAULT_NETWORK_ROLE), + }, + }, + ], + default: [], + _id: false, + validate: [ + { + validator: function (value) { + const maxLimit = 6; + return value.length <= maxLimit; + }, + message: "Too many networks. Maximum limit: 6.", + }, + ], + }, + group_roles: { + type: [ + { + group: { + type: ObjectId, + ref: "group", + default: mongoose.Types.ObjectId(constants.DEFAULT_GROUP), + }, + userType: { type: String, default: "guest", enum: validUserTypes }, + createdAt: { type: String, default: new Date() }, + role: { + type: ObjectId, + ref: "role", + default: mongoose.Types.ObjectId(constants.DEFAULT_GROUP_ROLE), + }, + }, + ], + default: [], + _id: false, + validate: [ + { + validator: function (value) { + const maxLimit = 6; + return value.length <= maxLimit; + }, + message: "Too many groups. Maximum limit: 6.", + }, + ], + }, + + permissions: [ + { + type: ObjectId, + ref: "permission", + }, + ], + organization: { + type: String, + default: "airqo", + }, + long_organization: { + type: String, + default: "airqo", + }, + rateLimit: { + type: Number, + }, + phoneNumber: { + type: Number, + validate: { + validator(phoneNumber) { + return !!phoneNumber || this.email; + }, + message: "Phone number or email is required!", + }, + }, + resetPasswordToken: { type: String }, + resetPasswordExpires: { type: Date }, + jobTitle: { + type: String, + }, + website: { type: String }, + description: { type: String }, + lastLogin: { + type: Date, + }, + category: { + type: String, + }, + notifications: { + email: { type: Boolean, default: false }, + push: { type: Boolean, default: false }, + text: { type: Boolean, default: false }, + phone: { type: Boolean, default: false }, + }, + profilePicture: { + type: String, + maxLength: 200, + validate: { + validator: function (v) { + const urlRegex = + /^(http(s)?:\/\/.)[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/g; + return urlRegex.test(v); + }, + message: + "Profile picture URL must be a valid URL & must not exceed 200 characters.", + }, + }, + google_id: { type: String, trim: true }, + timezone: { type: String, trim: true }, + }, + { timestamps: true } +); + +UserSchema.path("network_roles.userType").validate(function (value) { + return validUserTypes.includes(value); +}, "Invalid userType value"); + +UserSchema.path("group_roles.userType").validate(function (value) { + return validUserTypes.includes(value); +}, "Invalid userType value"); + +UserSchema.pre("save", async function (next) { + // Password hashing + if (this.isModified("password")) { + this.password = bcrypt.hashSync(this.password, saltRounds); + } + + // Validate contact information + if (!this.email && !this.phoneNumber) { + return next(new Error("Phone number or email is required!")); + } + + // Profile picture validation + if (this.profilePicture && !validateProfilePicture(this.profilePicture)) { + return next( + new HttpError("Bad Request Error", httpStatus.BAD_REQUEST, { + message: "Invalid profile picture URL", + }) + ); + } + + // Network roles handling + if (!this.network_roles || this.network_roles.length === 0) { + if ( + !constants || + !constants.DEFAULT_NETWORK || + !constants.DEFAULT_NETWORK_ROLE + ) { + throw new HttpError( + "Internal Server Error", + httpStatus.INTERNAL_SERVER_ERROR, + { + message: + "Contact support@airqo.net -- unable to retrieve the default Network or Role to which the User will belong", + } + ); + } + + this.network_roles = [ + { + network: mongoose.Types.ObjectId(constants.DEFAULT_NETWORK), + userType: "guest", + createdAt: new Date(), + role: mongoose.Types.ObjectId(constants.DEFAULT_NETWORK_ROLE), + }, + ]; + } + + // Group roles handling + if (!this.group_roles || this.group_roles.length === 0) { + if ( + !constants || + !constants.DEFAULT_GROUP || + !constants.DEFAULT_GROUP_ROLE + ) { + throw new HttpError( + "Internal Server Error", + httpStatus.INTERNAL_SERVER_ERROR, + { + message: + "Contact support@airqo.net -- unable to retrieve the default Group or Role", + } + ); + } + + this.group_roles = [ + { + group: mongoose.Types.ObjectId(constants.DEFAULT_GROUP), + userType: "guest", + createdAt: new Date(), + role: mongoose.Types.ObjectId(constants.DEFAULT_GROUP_ROLE), + }, + ]; + } + + // Permissions handling + if (this.permissions && this.permissions.length > 0) { + // Additional permissions validation can be added here if needed + this.permissions = [...new Set(this.permissions)]; // Ensure unique permissions + } + + // Ensure default values + this.verified = this.verified ?? false; + this.analyticsVersion = this.analyticsVersion ?? 2; + + return next(); +}); + +UserSchema.pre( + ["updateOne", "findOneAndUpdate", "updateMany", "update", "save"], + function (next) { + if (this.getUpdate) { + const updates = this.getUpdate(); + const fieldsToValidate = [ + "_id", + "firstName", + "lastName", + "userName", + "email", + "organization", + "long_organization", + "privilege", + "country", + "profilePicture", + "phoneNumber", + "createdAt", + "updatedAt", + "rateLimit", + "lastLogin", + "iat", + ]; + + fieldsToValidate.forEach((field) => { + // Check for empty/null/undefined values + const value = updates[field] || (updates.$set && updates.$set[field]); + + if (value === undefined || value === null || value === "") { + return next( + new HttpError("Validation Error", httpStatus.BAD_REQUEST, { + message: `${field} cannot be empty, null, or undefined`, + }) + ); + } + }); + if (updates) { + // Prevent modification of certain immutable fields + const immutableFields = ["firebase_uid", "email", "createdAt", "_id"]; + + immutableFields.forEach((field) => { + if (updates[field]) delete updates[field]; + + if (updates.$set && updates.$set[field]) { + return next( + new HttpError( + "Modification Not Allowed", + httpStatus.BAD_REQUEST, + { message: `Cannot modify ${field} after creation` } + ) + ); + } + + if (updates.$set) delete updates.$set[field]; + if (updates.$push) delete updates.$push[field]; + }); + + // Validate network roles and group roles limits + if ( + updates.network_roles && + updates.network_roles.length > ORGANISATIONS_LIMIT + ) { + return next( + new HttpError("Validation Error", httpStatus.BAD_REQUEST, { + message: `Maximum ${ORGANISATIONS_LIMIT} network roles allowed`, + }) + ); + } + + if ( + updates.group_roles && + updates.group_roles.length > ORGANISATIONS_LIMIT + ) { + return next( + new HttpError("Validation Error", httpStatus.BAD_REQUEST, { + message: `Maximum ${ORGANISATIONS_LIMIT} group roles allowed`, + }) + ); + } + + // Ensure password is hashed if modified + if (updates.password) { + updates.password = bcrypt.hashSync(updates.password, saltRounds); + } + } + } + + // Additional checks for new documents + if (this.isNew) { + const requiredFields = ["firstName", "lastName", "email"]; + requiredFields.forEach((field) => { + if (this.isModified(field) && !this[field]) { + return next(new Error(`${field} is required`)); + } + }); + } + + next(); + } +); + +UserSchema.index({ email: 1 }, { unique: true }); +UserSchema.index({ userName: 1 }, { unique: true }); + +UserSchema.statics = { + async register(args, next) { + try { + const data = await this.create({ + ...args, + }); + if (!isEmpty(data)) { + return { + success: true, + data, + message: "user created", + status: httpStatus.OK, + }; + } else if (isEmpty(data)) { + return { + success: true, + data, + message: "Operation successful but user NOT successfully created", + status: httpStatus.OK, + }; + } + } catch (err) { + logObject("the error", err); + let response = {}; + let message = "validation errors for some of the provided fields"; + let status = httpStatus.CONFLICT; + if (err.code === 11000) { + logObject("the err.code again", err.code); + const duplicate_record = args.email ? args.email : args.userName; + response[duplicate_record] = `${duplicate_record} must be unique`; + response["message"] = + "the email and userName must be unique for every user"; + try { + const email = args.email; + const firstName = args.firstName; + const lastName = args.lastName; + const emailResponse = await mailer.existingUserRegistrationRequest( + { + email, + firstName, + lastName, + }, + next + ); + if (emailResponse && emailResponse.success === false) { + logger.error( + `🐛🐛 Internal Server Error -- ${stringify(emailResponse)}` + ); + } + } catch (error) { + logger.error(`🐛🐛 Internal Server Error -- ${error.message}`); + } + } else if (err.keyValue) { + Object.entries(err.keyValue).forEach(([key, value]) => { + return (response[key] = `the ${key} must be unique`); + }); + } else if (err.errors) { + Object.entries(err.errors).forEach(([key, value]) => { + return (response[key] = value.message); + }); + } + logger.error(`🐛🐛 Internal Server Error -- ${err.message}`); + next(new HttpError(message, status, response)); + } + }, + async listStatistics(next) { + try { + const response = await this.aggregate() + .match({ email: { $nin: [null, ""] } }) + .sort({ createdAt: -1 }) + .lookup({ + from: "clients", + localField: "_id", + foreignField: "user_id", + as: "clients", + }) + .lookup({ + from: "users", + localField: "clients.user_id", + foreignField: "_id", + as: "api_clients", + }) + .group({ + _id: null, + users: { $sum: 1 }, + user_details: { + $push: { + userName: "$userName", + email: "$email", + firstName: "$firstName", + lastName: "$lastName", + _id: "$_id", + }, + }, + active_users: { $sum: { $cond: ["$isActive", 1, 0] } }, + active_user_details: { + $addToSet: { + $cond: { + if: "$isActive", + then: { + userName: "$userName", + email: "$email", + firstName: "$firstName", + lastName: "$lastName", + _id: "$_id", + }, + else: "$nothing", + }, + }, + }, + client_users: { $addToSet: "$clients.user_id" }, + api_user_details: { + $addToSet: { + userName: { $arrayElemAt: ["$api_clients.userName", 0] }, + email: { $arrayElemAt: ["$api_clients.email", 0] }, + firstName: { $arrayElemAt: ["$api_clients.firstName", 0] }, + lastName: { $arrayElemAt: ["$api_clients.lastName", 0] }, + _id: { $arrayElemAt: ["$api_clients._id", 0] }, + }, + }, + }) + .project({ + _id: 0, + users: { + number: "$users", + details: "$user_details", + }, + active_users: { + number: "$active_users", + details: "$active_user_details", + }, + api_users: { + number: { $size: { $ifNull: ["$client_users", []] } }, + details: "$api_user_details", + }, + }) + .allowDiskUse(true); + + if (!isEmpty(response)) { + return { + success: true, + message: "Successfully retrieved the user statistics", + data: response[0], + status: httpStatus.OK, + }; + } else if (isEmpty(response)) { + return { + success: true, + message: "No users statistics exist", + data: [], + status: httpStatus.OK, + }; + } + } catch (error) { + logger.error(`🐛🐛 Internal Server Error -- ${error.message}`); + next( + new HttpError( + "Internal Server Error", + httpStatus.INTERNAL_SERVER_ERROR, + { message: error.message } + ) + ); + } + }, + async list({ skip = 0, limit = 100, filter = {} } = {}, next) { + try { + const inclusionProjection = constants.USERS_INCLUSION_PROJECTION; + const exclusionProjection = constants.USERS_EXCLUSION_PROJECTION( + filter.category ? filter.category : "none" + ); + + if (!isEmpty(filter.category)) { + delete filter.category; + } + logObject("the filter being used", filter); + const response = await this.aggregate() + .match(filter) + .lookup({ + from: "permissions", + localField: "permissions", + foreignField: "_id", + as: "permissions", + }) + .lookup({ + from: "clients", + localField: "_id", + foreignField: "user_id", + as: "clients", + }) + .lookup({ + from: "networks", + localField: "_id", + foreignField: "net_manager", + as: "my_networks", + }) + .lookup({ + from: "groups", + localField: "_id", + foreignField: "grp_manager", + as: "my_groups", + }) + .addFields({ + createdAt: { + $dateToString: { + format: "%Y-%m-%d %H:%M:%S", + date: "$_id", + }, + }, + }) + .unwind({ + path: "$network_roles", + preserveNullAndEmptyArrays: true, + }) + .unwind({ + path: "$group_roles", + preserveNullAndEmptyArrays: true, + }) + .lookup({ + from: "networks", + localField: "network_roles.network", + foreignField: "_id", + as: "network", + }) + .lookup({ + from: "groups", + localField: "group_roles.group", + foreignField: "_id", + as: "group", + }) + .lookup({ + from: "roles", + localField: "network_roles.role", + foreignField: "_id", + as: "network_role", + }) + .lookup({ + from: "roles", + localField: "group_roles.role", + foreignField: "_id", + as: "group_role", + }) + .lookup({ + from: "permissions", + localField: "network_role.role_permissions", + foreignField: "_id", + as: "network_role_permissions", + }) + .lookup({ + from: "permissions", + localField: "group_role.role_permissions", + foreignField: "_id", + as: "group_role_permissions", + }) + .group({ + _id: "$_id", + firstName: { $first: "$firstName" }, + lastName: { $first: "$lastName" }, + lastLogin: { $first: "$lastLogin" }, + timezone: { $first: "$timezone" }, + isActive: { $first: "$isActive" }, + loginCount: { $first: "$loginCount" }, + userName: { $first: "$userName" }, + email: { $first: "$email" }, + verified: { $first: "$verified" }, + analyticsVersion: { $first: "$analyticsVersion" }, + country: { $first: "$country" }, + privilege: { $first: "$privilege" }, + website: { $first: "$website" }, + category: { $first: "$category" }, + organization: { $first: "$organization" }, + long_organization: { $first: "$long_organization" }, + rateLimit: { $first: "$rateLimit" }, + jobTitle: { $first: "$jobTitle" }, + description: { $first: "$description" }, + profilePicture: { $first: "$profilePicture" }, + phoneNumber: { $first: "$phoneNumber" }, + group_roles: { $first: "$group_roles" }, + network_roles: { $first: "$network_roles" }, + group_role: { $first: "$group_role" }, + network_role: { $first: "$network_role" }, + clients: { $first: "$clients" }, + groups: { + $addToSet: { + grp_title: { $arrayElemAt: ["$group.grp_title", 0] }, + _id: { $arrayElemAt: ["$group._id", 0] }, + createdAt: { $arrayElemAt: ["$group.createdAt", 0] }, + status: { $arrayElemAt: ["$group.grp_status", 0] }, + role: { + $cond: { + if: { $ifNull: ["$group_role", false] }, + then: { + _id: { $arrayElemAt: ["$group_role._id", 0] }, + role_name: { $arrayElemAt: ["$group_role.role_name", 0] }, + role_permissions: "$group_role_permissions", + }, + else: null, + }, + }, + userType: { + $cond: { + if: { $eq: [{ $type: "$group_roles.userType" }, "missing"] }, + then: "user", + else: "$group_roles.userType", + }, + }, + }, + }, + permissions: { $first: "$permissions" }, + my_networks: { $first: "$my_networks" }, + my_groups: { $first: "$my_groups" }, + createdAt: { $first: "$createdAt" }, + updatedAt: { $first: "$createdAt" }, + networks: { + $addToSet: { + net_name: { $arrayElemAt: ["$network.net_name", 0] }, + _id: { $arrayElemAt: ["$network._id", 0] }, + role: { + $cond: { + if: { $ifNull: ["$network_role", false] }, + then: { + _id: { $arrayElemAt: ["$network_role._id", 0] }, + role_name: { $arrayElemAt: ["$network_role.role_name", 0] }, + role_permissions: "$network_role_permissions", + }, + else: null, + }, + }, + userType: { + $cond: { + if: { + $eq: [{ $type: "$network_roles.userType" }, "missing"], + }, + then: "user", + else: "$network_roles.userType", + }, + }, + }, + }, + }) + .project(inclusionProjection) + .project(exclusionProjection) + .sort({ createdAt: -1 }) + .skip(skip ? skip : 0) + .limit(limit ? limit : parseInt(constants.DEFAULT_LIMIT)) + .allowDiskUse(true); + if (!isEmpty(response)) { + return { + success: true, + message: "successfully retrieved the user details", + data: response, + status: httpStatus.OK, + }; + } else if (isEmpty(response)) { + return { + success: true, + message: "no users exist", + data: [], + status: httpStatus.OK, + }; + } + } catch (error) { + logger.error(`🐛🐛 Internal Server Error -- ${error.message}`); + next( + new HttpError( + "Internal Server Error", + httpStatus.INTERNAL_SERVER_ERROR, + { message: error.message } + ) + ); + } + }, + async modify({ filter = {}, update = {} } = {}, next) { + try { + logText("the user modification function........"); + const options = { new: true }; + const fieldNames = Object.keys(update); + const fieldsString = fieldNames.join(" "); + + // Find and update user + const updatedUser = await this.findOneAndUpdate( + filter, + update, + options + ).select(fieldsString); + + // Handle update result + if (!isEmpty(updatedUser)) { + const { _id, ...userData } = updatedUser._doc; + return { + success: true, + message: "successfully modified the user", + data: userData, + status: httpStatus.OK, + }; + } + + // User not found + return next( + new HttpError("Bad Request Error", httpStatus.BAD_REQUEST, { + message: "user does not exist, please crosscheck", + }) + ); + } catch (error) { + logger.error(`🐛🐛 Internal Server Error -- ${error.message}`); + return next( + new HttpError( + "Internal Server Error", + httpStatus.INTERNAL_SERVER_ERROR, + { message: error.message } + ) + ); + } + }, + async remove({ filter = {} } = {}, next) { + try { + const options = { + projection: { + _id: 0, + email: 1, + firstName: 1, + lastName: 1, + lastLogin: 1, + }, + }; + const removedUser = await this.findOneAndRemove(filter, options).exec(); + + if (!isEmpty(removedUser)) { + return { + success: true, + message: "Successfully removed the user", + data: removedUser._doc, + status: httpStatus.OK, + }; + } else if (isEmpty(removedUser)) { + next( + new HttpError("Bad Request Error", httpStatus.BAD_REQUEST, { + message: "Provided User does not exist, please crosscheck", + }) + ); + } + } catch (error) { + logObject("the models error", error); + logger.error(`🐛🐛 Internal Server Error -- ${error.message}`); + next( + new HttpError( + "Internal Server Error", + httpStatus.INTERNAL_SERVER_ERROR, + { message: error.message } + ) + ); + } + }, +}; + +UserSchema.methods = { + authenticateUser(password) { + return bcrypt.compareSync(password, this.password); + }, + newToken() { + const token = accessCodeGenerator.generate( + constants.RANDOM_PASSWORD_CONFIGURATION(10) + ); + const hashedToken = bcrypt.hashSync(token, saltRounds); + return { + accessToken: hashedToken, + plainTextToken: `${token.id}|${plainTextToken}`, + }; + }, + async toAuthJSON() { + const token = await this.createToken(); + return { + _id: this._id, + userName: this.userName, + token: `JWT ${token}`, + email: this.email, + }; + }, + toJSON() { + return { + _id: this._id, + userName: this.userName, + email: this.email, + firstName: this.firstName, + organization: this.organization, + long_organization: this.long_organization, + group_roles: this.group_roles, + network_roles: this.network_roles, + privilege: this.privilege, + lastName: this.lastName, + country: this.country, + website: this.website, + category: this.category, + jobTitle: this.jobTitle, + profilePicture: this.profilePicture, + phoneNumber: this.phoneNumber, + description: this.description, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + role: this.role, + verified: this.verified, + analyticsVersion: this.analyticsVersion, + rateLimit: this.rateLimit, + lastLogin: this.lastLogin, + isActive: this.isActive, + loginCount: this.loginCount, + timezone: this.timezone, + }; + }, +}; + +const UserModel = (tenant) => { + try { + let users = mongoose.model("users"); + return users; + } catch (error) { + let users = getModelByTenant(tenant, "user", UserSchema); + return users; + } +}; +UserSchema.methods.createToken = async function () { + try { + const filter = { _id: this._id }; + const userWithDerivedAttributes = await UserModel("airqo").list({ filter }); + if ( + userWithDerivedAttributes.success && + userWithDerivedAttributes.success === false + ) { + logger.error( + `Internal Server Error -- ${JSON.stringify(userWithDerivedAttributes)}` + ); + return userWithDerivedAttributes; + } else { + const user = userWithDerivedAttributes.data[0]; + const oneDayExpiry = Math.floor(Date.now() / 1000) + 24 * 60 * 60; + const oneHourExpiry = Math.floor(Date.now() / 1000) + 60 * 60; + logObject("user", user); + return jwt.sign( + { + _id: user._id, + firstName: user.firstName, + lastName: user.lastName, + userName: user.userName, + email: user.email, + organization: user.organization, + long_organization: user.long_organization, + privilege: user.privilege, + role: user.role, + country: user.country, + profilePicture: user.profilePicture, + phoneNumber: user.phoneNumber, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + rateLimit: user.rateLimit, + lastLogin: user.lastLogin, + // exp: oneHourExpiry, + }, + constants.JWT_SECRET + ); + } + } catch (error) { + logger.error(`🐛🐛 Internal Server Error --- ${error.message}`); + } +}; + +module.exports = UserModel;