Skip to content

Commit

Permalink
Ft: auth (new user needs to verify one's account before login
Browse files Browse the repository at this point in the history
  • Loading branch information
MugemaneBertin2001 committed May 20, 2024
1 parent 49e409c commit 6dda574
Show file tree
Hide file tree
Showing 11 changed files with 279 additions and 7 deletions.
30 changes: 29 additions & 1 deletion src/controllers/userControllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export const createUserController = async (req: Request, res: Response) => {
}
return res.status(201).json({
status: 201,
message: "User successfully created."
message: "User successfully created. check your inbox to verify your email"
});
} catch (err: any) {
if (err.name === "UnauthorizedError" && err.message === "User already exists") {
Expand Down Expand Up @@ -190,6 +190,7 @@ export const handleSuccess = async (req: Request, res: Response) => {
name: user.displayName,
email: user.emails[0].value,
username: user.name.familyName,
isVerified:true,
//@ts-ignore
password: null
});
Expand Down Expand Up @@ -401,3 +402,30 @@ export const resetPasswordController = async (req: Request, res: Response): Prom
return res.status(500).json({ message: 'Internal server error.' });
}
};

export const verifyUserEmailController = async (req: Request, res: Response) => {
try {
// Extract email from the request body
const {email} = req.body;

const { error } = Emailschema.validate(req.body);
if (error) {
const cleanErrorMessage = error.details.map(detail => detail.message.replace(/['"]/g, '').trim()).join(', ')
return res.status(400).json({ message: cleanErrorMessage });
}
const result = await userService.verifyUserEmail(email);
res.status(result.status).json({ message: result.message });
} catch (error) {
res.status(500).json({ error: 'Failed to send verification email.' });
}
};
export const verifyUserController = async (req: Request, res: Response): Promise<void> => {
try {
// Extract token from the request query
const token = req.query.token as string;
const result = await userService.verifyNewUser(token);
res.status(result.status).json({ message: result.message });
} catch (error) {
res.status(500).json({ error: 'Failed to verify new user.' });
}
};
5 changes: 4 additions & 1 deletion src/docs/swagger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
sendResetLink,
updateForgotPassword,
verifyUserAccessToken,
verifyUserEmail,
} from "./users";
import {
getProducts,
Expand All @@ -39,7 +40,6 @@ import { getCategories, addCategories, getSingleCategory, updateCategories, dele
deleteRole
} from "./roledoc";
import { AddToWishes, deleteWish, getWishes, getWishesByProduct, wishSchema } from "./wishes";
import { joinChats } from "./chats";
import { addItemToCartDoc, clearAllProductFromCartDoc, removeProductFromCartDoc, updateProductQuantityDoc, viewCartDoc } from "./cart";
import { getAllNotifications, readNotification } from "./notifications";
import { homepage } from "./home";
Expand Down Expand Up @@ -114,6 +114,9 @@ const options = {
"/api/v1/users/reset-password": {
patch: updateForgotPassword,
},
"/api/v1/users/verify-user-email": {
post: verifyUserEmail,
},

"/api/v1/users/me": {
post: verifyUserAccessToken,
Expand Down
36 changes: 36 additions & 0 deletions src/docs/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,3 +481,39 @@ export const updateForgotPassword = {
}
}
};
export const verifyUserEmail = {
tags: ["Users"],
summary: "Verify user email",
description: "Verify user email using the provided email address",
requestBody: {
required: true,
content: {
"application/json": {
schema: {
type: "object",
properties: {
email: {
type: "string",
description: "The user's email address"
}
},
required: ["email"]
}
}
}
},
responses: {
200: {
description: "Verification email sent successfully"
},
400: {
description: "Invalid email address"
},
404: {
description: "User not found"
},
500: {
description: "Internal server error"
}
}
};
92 changes: 92 additions & 0 deletions src/email-templates/verifyUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// emailTemplates/verifyUser.ts
const verifyUserEmailTemplate = (username:string,verificationLink:string) => {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Verify Your Email</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
color: #333333;
}
.container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.header {
text-align: center;
padding-bottom: 20px;
}
.header img {
max-width: 100px;
}
.content {
font-size: 16px;
line-height: 1.5;
}
.btn {
display: inline-block;
margin-top: 20px;
padding: 10px 20px;
background-color: #007bff;
color: #ffffff;
text-decoration: none;
border-radius: 4px;
}
.footer {
margin-top: 20px;
font-size: 12px;
color: #666666;
text-align: center;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<img src="https://yourcompanylogo.com/logo.png" alt="Company Logo">
</div>
<div class="content">
<h1>Welcome, ${username}!</h1>
<p>Thank you for registering with us. Please verify your email address to complete your registration.</p>
<p>Click the button below to verify your email address:</p>
<a href="${verificationLink}" class="btn">Verify Email</a>
<p>If the button above does not work, copy and paste the following link into your browser:</p>
<p><a href="${verificationLink}">${verificationLink}</a></p>
</div>
<div class="footer">
<p>&copy; 2024 Your Company Name. All rights reserved.</p>
<p>If you did not register for this account, please ignore this email.</p>
</div>
</div>
</body>
</html>
`;


};

export default verifyUserEmailTemplate;


export const generateEmailVerificationEmail = (Username:string)=> {
return `
Dear ${Username},
Thank you for verifying your email address. You are now able to start using our system.
If you have any questions, feel free to reach out to our support team.
Best regards,
Your Company Name
`;
}
16 changes: 16 additions & 0 deletions src/middlewares/isVerified.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Request, Response, NextFunction } from 'express';
import User from '../sequelize/models/users';

export const isVerified = async (req: Request, res: Response, next: NextFunction) => {
// @ts-ignore
const { email} = req.body;
const user = await User.findOne({
where:{
email:email
}
});
if (user?.isVerified === false) {
return res.status(403).json({ message: 'Account is not verified' });
}
next();
};
10 changes: 6 additions & 4 deletions src/routes/userRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Router } from "express";
import { fetchAllUsers, createUserController, userLogin, updatePassword, tokenVerification, handleSuccess, handleFailure,updateProfileController, getProfileController, otpVerification, changeUserAccountStatus, logout, sendResetLinkEmail, resetPasswordController } from "../controllers/userControllers";
import { fetchAllUsers, createUserController, userLogin, updatePassword, tokenVerification, handleSuccess, handleFailure,updateProfileController, getProfileController, otpVerification,updateUserRole, changeUserAccountStatus, logout, sendResetLinkEmail, resetPasswordController, verifyUserEmailController, verifyUserController } from "../controllers/userControllers";
import { emailValidation, validateSchema } from "../middlewares/validator";
import { isLoggedIn } from "../middlewares/isLoggedIn";
import { passwordUpdateSchema } from "../schemas/passwordUpdate";
Expand All @@ -9,22 +9,21 @@ import logInSchema from "../schemas/loginSchema";
import { profileSchemas, signUpSchema } from "../schemas/signUpSchema";
import upload from "../middlewares/multer";
import isUploadedFileImage from "../middlewares/isImage";
import bodyParser from "body-parser";
import { isAdmin } from "../middlewares/isAdmin";
import { roleUpdateSchema } from "../schemas/userRoleUpdateSchema";
import { updateUserRole } from "../controllers/userControllers";
import { roleExist } from "../middlewares/roleExist";
import { userExist } from "../middlewares/userExist";
import { isDisabled } from "../middlewares/isDisabled";
import { verifyToken } from "../middlewares/verifyToken";
import { isVerified } from "../middlewares/isVerified";



const userRoutes = Router();

userRoutes.get("/", fetchAllUsers);
userRoutes.put("/passwordupdate", isLoggedIn, validateSchema(passwordUpdateSchema), updatePassword)
userRoutes.post("/login", emailValidation,validateSchema(logInSchema),isDisabled,userLogin);
userRoutes.post("/login", emailValidation,validateSchema(logInSchema),isDisabled,isVerified,userLogin);
userRoutes.post("/register", emailValidation, validateSchema(signUpSchema), createUserController);
userRoutes.put("/passwordupdate", isLoggedIn, validateSchema(passwordUpdateSchema), updatePassword);
userRoutes.get("/2fa-verify/:token",tokenVerification);
Expand All @@ -51,6 +50,9 @@ userRoutes.get("/auth/google/success", handleSuccess);
userRoutes.get("/auth/google/failure", handleFailure);
userRoutes.post('/password-reset-link', sendResetLinkEmail);
userRoutes.patch('/reset-password', resetPasswordController);
userRoutes.post('/verify-user-email', verifyUserEmailController);
userRoutes.get('/verify-user', verifyUserController);


userRoutes.get("/me", verifyToken);

Expand Down
23 changes: 23 additions & 0 deletions src/sequelize/migrations/h20240519174339-add-is-verified.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use strict';

/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
const isVerifiedColumnExists = await queryInterface.describeTable("users").then((columns) => {
return "isAvailable" in columns;
});

if (!isVerifiedColumnExists) {
await queryInterface.addColumn("users", "isVerified", {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false,
});
}
},

async down(queryInterface, Sequelize) {
await queryInterface.removeColumn("users", "isVerified");
},
};

7 changes: 7 additions & 0 deletions src/sequelize/models/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface UserAttributes {
password: string | undefined;
roleId: number | undefined;
isActive?:boolean;
isVerified?:boolean;
createdAt?: Date;
updatedAt?: Date;
}
Expand All @@ -24,6 +25,7 @@ class User extends Model<UserAttributes> implements UserAttributes {
email!: string;
password!: string;
isActive: boolean | undefined;
isVerified?: boolean | undefined;
roleId!: number | undefined;
createdAt!: Date | undefined;
updatedAt!: Date | undefined;
Expand Down Expand Up @@ -67,6 +69,11 @@ User.init(
allowNull:false,
defaultValue:true
},
isVerified:{
type: DataTypes.BOOLEAN,
allowNull:false,
defaultValue:false
},
createdAt: {
allowNull: false,
type: DataTypes.DATE,
Expand Down
4 changes: 4 additions & 0 deletions src/sequelize/seeders/b20240412144111-demo-user.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ module.exports = {
email: "[email protected]",
password: await bcrypt.hash("soleil00", 10),
roleId: 3,
isVerified:true,
createdAt: new Date(),
updatedAt: new Date(),
},
Expand All @@ -39,6 +40,7 @@ module.exports = {
email: "[email protected]",
password: await bcrypt.hash("soleil00", 10),
roleId: 1,
isVerified:true,
createdAt: new Date(),
updatedAt: new Date(),
},
Expand All @@ -48,6 +50,7 @@ module.exports = {
email: "[email protected]",
password: await bcrypt.hash("Test@123", 10),
roleId: 2,
isVerified:true,
createdAt: new Date(),
updatedAt: new Date(),
},
Expand All @@ -57,6 +60,7 @@ module.exports = {
email: "[email protected]",
password: await bcrypt.hash("Test@123", 10),
roleId: 2,
isVerified:true,
createdAt: new Date(),
updatedAt: new Date(),
},
Expand Down
Loading

0 comments on commit 6dda574

Please sign in to comment.