Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

move to production #3913

Merged
merged 10 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion k8s/analytics/values-prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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-6fe30608-1732546291
tag: prod-eccfdc59-1732550643
api:
name: airqo-analytics-api
label: analytics-api
Expand Down
2 changes: 1 addition & 1 deletion k8s/auth-service/values-prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ app:
replicaCount: 3
image:
repository: eu.gcr.io/airqo-250220/airqo-auth-api
tag: prod-6fe30608-1732546291
tag: prod-eccfdc59-1732550643
nameOverride: ''
fullnameOverride: ''
podAnnotations: {}
Expand Down
2 changes: 1 addition & 1 deletion k8s/auth-service/values-stage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ app:
replicaCount: 2
image:
repository: eu.gcr.io/airqo-250220/airqo-stage-auth-api
tag: stage-e0815cc8-1731824148
tag: stage-bd23841e-1732556820
nameOverride: ''
fullnameOverride: ''
podAnnotations: {}
Expand Down
2 changes: 1 addition & 1 deletion k8s/device-registry/values-prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ app:
replicaCount: 3
image:
repository: eu.gcr.io/airqo-250220/airqo-device-registry-api
tag: prod-6fe30608-1732546291
tag: prod-eccfdc59-1732550643
nameOverride: ''
fullnameOverride: ''
podAnnotations: {}
Expand Down
2 changes: 1 addition & 1 deletion k8s/exceedance/values-prod-airqo.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ app:
configmap: env-exceedance-production
image:
repository: eu.gcr.io/airqo-250220/airqo-exceedance-job
tag: prod-6fe30608-1732546291
tag: prod-eccfdc59-1732550643
nameOverride: ''
fullnameOverride: ''
2 changes: 1 addition & 1 deletion k8s/exceedance/values-prod-kcca.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ app:
configmap: env-exceedance-production
image:
repository: eu.gcr.io/airqo-250220/kcca-exceedance-job
tag: prod-6fe30608-1732546291
tag: prod-eccfdc59-1732550643
nameOverride: ''
fullnameOverride: ''
2 changes: 1 addition & 1 deletion k8s/predict/values-prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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-6fe30608-1732546291
tag: prod-eccfdc59-1732550643
api:
name: airqo-prediction-api
label: prediction-api
Expand Down
2 changes: 1 addition & 1 deletion k8s/website/values-stage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ app:
replicaCount: 2
image:
repository: eu.gcr.io/airqo-250220/airqo-stage-website-api
tag: stage-db11491e-1732546250
tag: stage-d205505d-1732550603
nameOverride: ''
fullnameOverride: ''
podAnnotations: {}
Expand Down
201 changes: 135 additions & 66 deletions src/auth-service/models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ 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();
Expand Down Expand Up @@ -247,14 +248,27 @@ UserSchema.path("group_roles.userType").validate(function (value) {
return validUserTypes.includes(value);
}, "Invalid userType value");

UserSchema.pre("save", function (next) {
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 ||
Expand All @@ -281,6 +295,7 @@ UserSchema.pre("save", function (next) {
];
}

// Group roles handling
if (!this.group_roles || this.group_roles.length === 0) {
if (
!constants ||
Expand All @@ -292,7 +307,7 @@ UserSchema.pre("save", function (next) {
httpStatus.INTERNAL_SERVER_ERROR,
{
message:
"Contact [email protected] -- unable to retrieve the default Group or Role to which the User will belong",
"Contact [email protected] -- unable to retrieve the default Group or Role",
}
);
}
Expand All @@ -307,23 +322,119 @@ UserSchema.pre("save", function (next) {
];
}

if (!this.verified) {
this.verified = false;
// 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.analyticsVersion) {
this.analyticsVersion = 2;
}
// Ensure default values
this.verified = this.verified ?? false;
this.analyticsVersion = this.analyticsVersion ?? 2;

return next();
});

UserSchema.pre("update", function (next) {
if (this.isModified("password")) {
this.password = bcrypt.hashSync(this.password, saltRounds);
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();
}
return next();
});
);

UserSchema.index({ email: 1 }, { unique: true });
UserSchema.index({ userName: 1 }, { unique: true });
Expand Down Expand Up @@ -702,61 +813,18 @@ UserSchema.statics = {
async modify({ filter = {}, update = {} } = {}, next) {
try {
logText("the user modification function........");
let options = { new: true };
const 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,
modifiedUpdate,
update,
options
).select(fieldsString);

// Handle update result
if (!isEmpty(updatedUser)) {
const { _id, ...userData } = updatedUser._doc;
return {
Expand All @@ -765,16 +833,17 @@ 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}`);
next(
return next(
new HttpError(
"Internal Server Error",
httpStatus.INTERNAL_SERVER_ERROR,
Expand Down
Loading