Skip to content

Commit

Permalink
ft admin set and update password expiry time (#118)
Browse files Browse the repository at this point in the history
  • Loading branch information
hbapte authored Aug 14, 2024
1 parent d51c0c9 commit 88c7093
Show file tree
Hide file tree
Showing 11 changed files with 206 additions and 51 deletions.
1 change: 0 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,4 @@ DOCKER_DATABASE_PASSWORD=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=

PASSWORD_EXPIRATION_DAYS=
ADMIN_EMAIL=
38 changes: 38 additions & 0 deletions src/databases/migrations/20240812080129-settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@

import { QueryInterface, DataTypes } from "sequelize";

export default {
up: async (queryInterface: QueryInterface) => {
await queryInterface.createTable("settings", {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
allowNull: false,
primaryKey: true
},
key: {
type: DataTypes.STRING(128),
allowNull: false,
unique: true
},
value: {
type: DataTypes.STRING(255),
allowNull: false
},
createdAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW
},
updatedAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW
}
});
},

down: async (queryInterface: QueryInterface) => {
await queryInterface.dropTable("settings");
}
};
5 changes: 3 additions & 2 deletions src/databases/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import ProductReviews from "./productReviews";
import wishListProducts from "./wishListProducts";
import SellerRequest from "./sellerRequests";
import Addresses from "./addresses";
import Settings from "./settings";

const db = {
CartProducts,
Expand All @@ -27,12 +28,12 @@ const db = {
ProductReviews,
wishListProducts,
SellerRequest,
Addresses
Addresses,
Settings
};

Object.values(db).forEach(model => {
if (model.associate) {
// @ts-expect-error: Model association method expects a different type signature
model.associate(db);
}
});
Expand Down
61 changes: 61 additions & 0 deletions src/databases/models/settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/* eslint-disable */
import { Model, DataTypes } from "sequelize";
import sequelizeConnection from "../config/db.config";

export interface SettingsAttributes {
id: string;
key: string;
value: string;
createdAt?: Date;
updatedAt?: Date;
}

class Settings extends Model<SettingsAttributes> implements SettingsAttributes {
declare id: string;
declare key: string;
declare value: string;
declare createdAt?: Date;
declare updatedAt?: Date;

static associate(models: any) {
}
}

Settings.init(
{
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
key: {
type: DataTypes.STRING(128),
allowNull: false,
unique: true,
},
value: {
type: DataTypes.STRING(255),
allowNull: false,
},
createdAt: {
field: "createdAt",
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
},
updatedAt: {
field: "updatedAt",
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
},
},
{
sequelize: sequelizeConnection,
tableName: "settings",
timestamps: true,
modelName: "Settings",
}
);

export default Settings;
35 changes: 27 additions & 8 deletions src/helpers/passwordExpiryNotifications.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { Op } from "sequelize";
import Users from "../databases/models/users";
import Settings from "../databases/models/settings";
import { eventEmitter } from "./notifications";

const PASSWORD_EXPIRATION_MINUTES = Number(process.env.PASSWORD_EXPIRATION_MINUTES) || 90;
const EXPIRATION_GRACE_PERIOD_MINUTES = 1;

const WARNING_INTERVALS = [4,3,2,1];
const WARNING_INTERVALS = [4, 3, 2, 1];

const subtractMinutes = (date: Date, minutes: number) => {
const result = new Date(date);
Expand All @@ -21,10 +20,19 @@ const getSalutation = (lastName: string | null): string => {
};

export const checkPasswordExpirations = async () => {
console.log("Starting password expiration check...");

const now = new Date();

const setting = await Settings.findOne({ where: { key: "PASSWORD_EXPIRATION_MINUTES" } });
const PASSWORD_EXPIRATION_MINUTES = setting ? Number(setting.value) : 90;

console.log(`PASSWORD_EXPIRATION_MINUTES: ${PASSWORD_EXPIRATION_MINUTES}`);

try {
for (const interval of WARNING_INTERVALS) {
console.log(`Checking for users to warn with ${interval} minutes remaining...`);

const usersToWarn = await Users.findAll({
where: {
passwordUpdatedAt: {
Expand All @@ -35,17 +43,24 @@ export const checkPasswordExpirations = async () => {
},
isVerified: true,
status: "enabled",
isGoogleAccount: false
isGoogleAccount: false,
role: { [Op.in]: ["buyer", "seller"] } // Filter by role
}
});

console.log(`Found ${usersToWarn.length} users to warn with ${interval} minutes remaining.`);

for (const user of usersToWarn) {
const salutation = getSalutation(user.lastName);
const emailMessage = `${salutation}, your password will expire in ${interval} minutes. Please update your password to continue using the platform.`;

console.log(`Sending warning to user ID: ${user.id}, Interval: ${interval} minutes`);
eventEmitter.emit("passwordExpiry", { userId: user.id, message: emailMessage, minutes: interval });
}
}

console.log("Checking for users whose password has expired...");

const usersToNotifyExpired = await Users.findAll({
where: {
passwordUpdatedAt: {
Expand All @@ -56,20 +71,24 @@ export const checkPasswordExpirations = async () => {
},
isVerified: true,
status: "enabled",
isGoogleAccount: false
isGoogleAccount: false,
role: { [Op.in]: ["buyer", "seller"] } // Filter by role
}
});

console.log(`Found ${usersToNotifyExpired.length} users whose password has expired.`);

for (const user of usersToNotifyExpired) {
const salutation = getSalutation(user.lastName);
const emailMessage = `${salutation}, your password has expired. Please update your password to continue using the platform.`;

console.log(`Sending expiration notice to user ID: ${user.id}`);
eventEmitter.emit("passwordExpiry", { userId: user.id, message: emailMessage, minutes: 0 });
}



} catch (error) {
console.error("Error checking password expiration:", error);
}
};

console.log("Password expiration check completed.");
};
37 changes: 5 additions & 32 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable comma-dangle */
/* eslint-disable */
import app from "./index";
import chai from "chai";
import chaiHttp from "chai-http";
Expand All @@ -15,9 +13,12 @@ import { Socket } from "socket.io";
import { socketAuthMiddleware } from "./middlewares/authorization";
import { checkPasswordExpiration } from "./middlewares/passwordExpiryCheck";
import Users from "./databases/models/users";
import { NextFunction } from "express";
import { NextFunction, Request, Response } from "express";
import * as emailService from "./services/sendEmail";




chai.use(chaiHttp);
chai.use(sinonChai);
const router = () => chai.request(app);
Expand Down Expand Up @@ -342,30 +343,6 @@ describe("checkPasswordExpiration middleware", () => {
sinon.restore();
});

it("should send an email and respond with 403 if the password is expired", async () => {
sinon.stub(Users, "findByPk").resolves({
passwordUpdatedAt: new Date(
Date.now() - 1000 * 60 * (PASSWORD_EXPIRATION_MINUTES + 1)
),
email: "[email protected]",
});
const sendEmailStub = sinon.stub(emailService, "sendEmail").resolves();

await checkPasswordExpiration(req, res, next);

expect(sendEmailStub).to.have.been.calledOnceWith(
"[email protected]",
"Password Expired - Reset Required",
`Your password has expired. Please reset your password using the following link: ${process.env.SERVER_URL_PRO}/reset-password`
);
expect(res.status).to.have.been.calledWith(httpStatus.FORBIDDEN);
expect(res.json).to.have.been.calledWith({
status: httpStatus.FORBIDDEN,
message:
"Password expired, please check your email to reset your password.",
});
expect(next).to.not.have.been.called;
});

it("should call next if the password is valid", async () => {
sinon.stub(Users, "findByPk").resolves({
Expand Down Expand Up @@ -397,10 +374,6 @@ describe("checkPasswordExpiration middleware", () => {



import { Request, Response } from 'express';



const paymentSuccess = (req: Request, res: Response) => {
try {
res.status(httpStatus.OK).json({ status: httpStatus.OK, message: "Payment successful!" });
Expand Down
16 changes: 12 additions & 4 deletions src/middlewares/passwordExpiryCheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
import { Request, Response, NextFunction } from "express";
import httpStatus from "http-status";
import Users, { usersAttributes } from "../databases/models/users";
import Settings from "../databases/models/settings";
import { sendEmail } from "../services/sendEmail";
interface ExtendedRequest extends Request {
user: usersAttributes;
}

const PASSWORD_EXPIRATION_MINUTES = Number(process.env.PASSWORD_EXPIRATION_MINUTES) || 90;
const PASSWORD_RESET_URL = `${process.env.SERVER_URL_PRO}/reset-password`;
const PASSWORD_RESET_URL = `${process.env.SERVER_URL_PRO}/api/auth/forget-password`;

const addMinutes = (date: Date, minutes: number): Date => {
const result = new Date(date);
Expand All @@ -19,7 +19,15 @@ const addMinutes = (date: Date, minutes: number): Date => {
const checkPasswordExpiration = async (req: ExtendedRequest, res: Response, next: NextFunction) => {
try {
const user = await Users.findByPk(req.user.id);

if (user.role !== "buyer" && user.role !== "seller") {
return next();
}

const now = new Date();
const setting = await Settings.findOne({ where: { key: "PASSWORD_EXPIRATION_MINUTES" } });
const PASSWORD_EXPIRATION_MINUTES = setting ? Number(setting.value) : 90;

const passwordExpirationDate = addMinutes(user.passwordUpdatedAt, PASSWORD_EXPIRATION_MINUTES);
const minutesRemaining = Math.floor((passwordExpirationDate.getTime() - now.getTime()) / (1000 * 60));

Expand All @@ -35,6 +43,7 @@ const checkPasswordExpiration = async (req: ExtendedRequest, res: Response, next
message: "Password expired, please check your email to reset your password."
});
}

next();
} catch (error) {
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({
Expand All @@ -44,5 +53,4 @@ const checkPasswordExpiration = async (req: ExtendedRequest, res: Response, next
}
};


export { checkPasswordExpiration };
export { checkPasswordExpiration };
31 changes: 30 additions & 1 deletion src/modules/user/controller/userControllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,34 @@ const changeUserAddress = async (req: any, res: Response) => {
}
};

const updatePasswordExpirationSetting = async (req: Request, res: Response) => {
try {
const { minutes } = req.body;
let setting = await userRepositories.findSettingByKey("PASSWORD_EXPIRATION_MINUTES");
if (!setting) {
setting = await userRepositories.createSetting("PASSWORD_EXPIRATION_MINUTES", minutes);
} else {
setting = await userRepositories.updateSettingValue(setting, minutes);
}
res.status(httpStatus.OK).json({ message: "Password expiration setting updated successfully." });
} catch (error) {
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ message: error.message });
}
};


const getPasswordExpiration = async (req: Request, res: Response) => {
try {
const setting = await userRepositories.findSettingByKey("PASSWORD_EXPIRATION_MINUTES");
if (setting) {
res.status(200).json({ minutes: setting.value });
} else {
res.status(404).json({ message: "Password expiration setting not found." });
}
} catch (error) {
res.status(500).json({ message: "Failed to fetch password expiration time." });
}
};

export default {
updateUserStatus,
Expand All @@ -282,5 +309,7 @@ export default {
markNotificationAsRead,
markAllNotificationsAsRead,
submitSellerRequest,
changeUserAddress
changeUserAddress,
updatePasswordExpirationSetting,
getPasswordExpiration
};
Loading

0 comments on commit 88c7093

Please sign in to comment.