Skip to content

Commit

Permalink
Refactoring and Enhancement Summary for Token Management System
Browse files Browse the repository at this point in the history
  • Loading branch information
Baalmart committed Oct 12, 2024
1 parent 0cb017a commit 5344f48
Show file tree
Hide file tree
Showing 8 changed files with 385 additions and 39 deletions.
63 changes: 43 additions & 20 deletions src/auth-service/bin/jobs/token-expiration-job.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
const AccessTokenModel = require("@models/AccessToken");
const cron = require("node-cron");
const constants = require("@config/constants");
const { logObject, logText } = require("@utils/log");
const mailer = require("@utils/mailer");
const stringify = require("@utils/stringify");
const log4js = require("log4js");
const logger = log4js.getLogger(
`${constants.ENVIRONMENT} -- bin/jobs/token-expiration-job`
);
const moment = require("moment-timezone");

async function sendEmailsInBatches(tokens, batchSize = 100) {
for (let i = 0; i < tokens.length; i += batchSize) {
const batch = tokens.slice(i, i + batchSize);
const emailPromises = batch.map((token) => {
const email = token.email;
const firstName = token.firstName;
const lastName = token.lastName;
logObject("the expiring token", token);
const {
user: { email, firstName, lastName },
} = token;

logObject("the email to be used", email);
return mailer
.expiringToken({ email, firstName, lastName })
.then((response) => {
if (response && response.success === false) {
logger.error(
`Error sending email to ${email}: ${stringify(response)}`
`🐛🐛 Error sending email to ${email}: ${stringify(response)}`
);
}
});
Expand All @@ -30,24 +33,44 @@ async function sendEmailsInBatches(tokens, batchSize = 100) {
}
}

const weeklyOnSundayAtMidnight = "0 0 * * 0";
cron.schedule(
weeklyOnSundayAtMidnight,
async () => {
const timeZone = moment.tz.guess();
const now = moment().tz(timeZone);
const twoMonthsFromNow = now.clone().add(2, "months");
const filter = {
expires: { $gte: now.toDate(), $lt: twoMonthsFromNow.toDate() },
};
async function fetchAllExpiringTokens() {
let allTokens = [];
let skip = 0;
const limit = 100;
let hasMoreTokens = true;

while (hasMoreTokens) {
const tokensResponse = await AccessTokenModel("airqo").getExpiringTokens({
skip,
limit,
});

try {
const tokens = await AccessTokenModel("airqo").list({ filter });
if (tokensResponse.success && tokensResponse.data.length > 0) {
allTokens = allTokens.concat(tokensResponse.data);
skip += limit; // Increment skip for the next batch
} else {
hasMoreTokens = false; // No more tokens to fetch
}
}
return allTokens;
}

const sendAlertsForExpiringTokens = async () => {
try {
const tokens = await fetchAllExpiringTokens();
if (tokens.length > 0) {
await sendEmailsInBatches(tokens);
} catch (error) {
logger.error(`🐛🐛 Internal Server Error -- ${stringify(error)}`);
} else {
logger.info("No expiring tokens found for this month.");
}
},
} catch (error) {
logger.error(`🐛🐛 Internal Server Error -- ${stringify(error)}`);
}
};

cron.schedule(
"0 0 */30 * *", // Every 30 days at midnight
sendAlertsForExpiringTokens,
{
scheduled: true,
timezone: "Africa/Nairobi",
Expand Down
1 change: 1 addition & 0 deletions src/auth-service/config/global/db-projections.js
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,7 @@ const dbProjections = {
last_used_at: 1,
isActive: 1,
expires: 1,
expiredEmailSent: 1,
name: 1,
permissions: 1,
scopes: 1,
Expand Down
102 changes: 102 additions & 0 deletions src/auth-service/controllers/create-token.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,108 @@ const createAccessToken = {
return;
}
},
listExpired: async (req, res, next) => {
try {
const errors = extractErrorsFromRequest(req);
if (errors) {
next(
new HttpError("bad request errors", httpStatus.BAD_REQUEST, errors)
);
return;
}

const request = req;
const defaultTenant = constants.DEFAULT_TENANT || "airqo";
request.query.tenant = isEmpty(req.query.tenant)
? defaultTenant
: req.query.tenant;

const result = await controlAccessUtil.listExpiredTokens(request, next);

if (isEmpty(result) || res.headersSent) {
return;
}

if (result.success === true) {
const status = result.status ? result.status : httpStatus.OK;
return res.status(status).json({
message: result.message ? result.message : "",
tokens: result.data ? result.data : [],
});
} else if (result.success === false) {
const status = result.status
? result.status
: httpStatus.INTERNAL_SERVER_ERROR;
return res.status(status).json({
message: result.message,
errors: result.errors
? result.errors
: { message: "Internal Server Error" },
});
}
} catch (error) {
logger.error(`🐛🐛 Internal Server Error ${error.message}`);
next(
new HttpError(
"Internal Server Error",
httpStatus.INTERNAL_SERVER_ERROR,
{ message: error.message }
)
);
return;
}
},
listExpiring: async (req, res, next) => {
try {
const errors = extractErrorsFromRequest(req);
if (errors) {
next(
new HttpError("bad request errors", httpStatus.BAD_REQUEST, errors)
);
return;
}

const request = req;
const defaultTenant = constants.DEFAULT_TENANT || "airqo";
request.query.tenant = isEmpty(req.query.tenant)
? defaultTenant
: req.query.tenant;

const result = await controlAccessUtil.listExpiringTokens(request, next);

if (isEmpty(result) || res.headersSent) {
return;
}

if (result.success === true) {
const status = result.status ? result.status : httpStatus.OK;
return res.status(status).json({
message: result.message ? result.message : "",
tokens: result.data ? result.data : [],
});
} else if (result.success === false) {
const status = result.status
? result.status
: httpStatus.INTERNAL_SERVER_ERROR;
return res.status(status).json({
message: result.message,
errors: result.errors
? result.errors
: { message: "Internal Server Error" },
});
}
} catch (error) {
logger.error(`🐛🐛 Internal Server Error ${error.message}`);
next(
new HttpError(
"Internal Server Error",
httpStatus.INTERNAL_SERVER_ERROR,
{ message: error.message }
)
);
return;
}
},
verify: async (req, res, next) => {
try {
const errors = extractErrorsFromRequest(req);
Expand Down
130 changes: 130 additions & 0 deletions src/auth-service/models/AccessToken.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const log4js = require("log4js");
const logger = log4js.getLogger(`${constants.ENVIRONMENT} -- token-model`);
const { HttpError } = require("@utils/errors");
const { getModelByTenant } = require("@config/database");
const moment = require("moment-timezone");

const toMilliseconds = (hrs, min, sec) => {
return (hrs * 60 * 60 + min * 60 + sec) * 1000;
Expand Down Expand Up @@ -40,6 +41,10 @@ const AccessTokenSchema = new mongoose.Schema(
type: Date,
required: [true, "expiry date is required!"],
},
expiredEmailSent: {
type: Boolean,
default: false,
},
},
{ timestamps: true }
);
Expand Down Expand Up @@ -222,6 +227,131 @@ AccessTokenSchema.statics = {
);
}
},
async getExpiredTokens({ skip = 0, limit = 100, filter = {} } = {}, next) {
try {
const inclusionProjection = constants.TOKENS_INCLUSION_PROJECTION;
const exclusionProjection = constants.TOKENS_EXCLUSION_PROJECTION(
filter.category ? filter.category : "none"
);

if (!isEmpty(filter.category)) {
delete filter.category;
}
const currentDate = moment().tz(moment.tz.guess()).toDate(); // Get current date in the user's timezone
const response = await this.aggregate()
.match({
expires: { $lt: currentDate },
...filter,
})
.lookup({
from: "clients",
localField: "client_id",
foreignField: "_id",
as: "client",
})
.lookup({
from: "users",
localField: "client.user_id",
foreignField: "_id",
as: "user",
})
.sort({ createdAt: -1 })
.project(inclusionProjection)
.project(exclusionProjection)
.skip(skip)
.limit(limit)
.allowDiskUse(true);

if (!isEmpty(response)) {
return {
success: true,
message: "Successfully retrieved expired tokens",
data: response,
status: httpStatus.OK,
};
} else {
return {
success: true,
message: "No expired tokens found",
status: httpStatus.NOT_FOUND,
data: [],
};
}
} catch (error) {
logger.error(`🐛🐛 Internal Server Error ${error.message}`);
next(
new HttpError(
"Internal Server Error",
httpStatus.INTERNAL_SERVER_ERROR,
{ message: error.message }
)
);
}
},
async getExpiringTokens({ skip = 0, limit = 100, filter = {} } = {}, next) {
try {
const inclusionProjection = constants.TOKENS_INCLUSION_PROJECTION;
const exclusionProjection = constants.TOKENS_EXCLUSION_PROJECTION(
filter.category ? filter.category : "none"
);

if (!isEmpty(filter.category)) {
delete filter.category;
}
const currentDate = moment().tz(moment.tz.guess()).toDate(); // current date in the user's timezone
const oneMonthFromNow = moment(currentDate).add(1, "month").toDate(); // one month from now

const response = await this.aggregate()
.match({
expires: { $gt: currentDate, $lt: oneMonthFromNow },
...filter,
})
.lookup({
from: "clients",
localField: "client_id",
foreignField: "_id",
as: "client",
})
.lookup({
from: "users",
localField: "client.user_id",
foreignField: "_id",
as: "user",
})
.sort({ createdAt: -1 })
.project(inclusionProjection)
.project(exclusionProjection)
.skip(skip)
.limit(limit)
.allowDiskUse(true);

if (!isEmpty(response)) {
return {
success: true,
message:
"Successfully retrieved tokens expiring within the next month",
data: response,
status: httpStatus.OK,
};
} else {
return {
success: true,
message: "No tokens found expiring within the next month",
status: httpStatus.NOT_FOUND,
data: [],
};
}
} 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 {
let options = { new: true };
Expand Down
Loading

0 comments on commit 5344f48

Please sign in to comment.