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

ft admin set and update password expiry time #118

Merged
merged 1 commit into from
Aug 14, 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
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
Loading