From db4db9bfec2a591def40e35c417289399b8d795d Mon Sep 17 00:00:00 2001
From: aimedivin
Date: Fri, 21 Jun 2024 20:52:30 +0200
Subject: [PATCH] fix(google-auth): resolve API redirect issue on same port
during google authentication - ensure correct redirect to client URL - send
appropriate response to client
fix(google-auth): resolve API redirect issue on same port during google authentication
- ensure correct redirect to client URL
- send appropriate response to client
---
src/__test__/categories.test.ts | 43 ++++++++++++++++
src/__test__/searchProduct.test.ts | 6 +--
src/__test__/vendorProduct.test.ts | 8 ---
src/controllers/productController.ts | 4 ++
src/entities/Category.ts | 6 ++-
src/entities/Product.ts | 2 +-
src/entities/User.ts | 4 +-
src/index.ts | 2 +-
src/routes/ProductRoutes.ts | 4 +-
src/routes/UserRoutes.ts | 49 ++++++++++++++-----
src/services/index.ts | 1 +
src/services/productServices/getCategories.ts | 24 +++++++++
.../productServices/listAllProductsService.ts | 18 ++-----
src/services/productServices/productStatus.ts | 1 -
src/services/productServices/searchProduct.ts | 3 +-
.../productServices/viewSingleProduct.ts | 4 --
.../sendResetPasswordLinkService.ts | 2 +-
src/services/userServices/userLoginService.ts | 5 +-
.../userServices/userProfileUpdateServices.ts | 2 -
.../userServices/userRegistrationService.ts | 2 +-
src/services/userServices/userResendOTP.ts | 2 -
src/services/userServices/userValidateOTP.ts | 4 +-
src/utils/auth.ts | 5 +-
23 files changed, 143 insertions(+), 58 deletions(-)
create mode 100644 src/__test__/categories.test.ts
create mode 100644 src/services/productServices/getCategories.ts
diff --git a/src/__test__/categories.test.ts b/src/__test__/categories.test.ts
new file mode 100644
index 0000000..ead7cc6
--- /dev/null
+++ b/src/__test__/categories.test.ts
@@ -0,0 +1,43 @@
+import request from 'supertest';
+import { app, server } from '../index';
+import { createConnection, getRepository} from 'typeorm';
+import { cleanDatabase } from './test-assets/DatabaseCleanup';
+import { Category } from '../entities/Category';
+
+beforeAll(async () => {
+ await createConnection();
+});
+
+jest.setTimeout(20000);
+
+afterAll(async () => {
+ await cleanDatabase();
+ server.close();
+});
+
+
+describe('GET /categories', () => {
+ it('should return all categories', async () => {
+ const categoryRepository = getRepository(Category);
+ await categoryRepository.save([
+ { name: 'Category 1' },
+ { name: 'Category 2' },
+ ]);
+
+ const response = await request(app).get('/product/categories');
+ console.log(response.error)
+ expect(response.status).toBe(200);
+ expect(response.body.status).toBe('success');
+ expect(response.body.categories).toHaveLength(2);
+ expect(response.body.categories[0].name).toBe('Category 1');
+ expect(response.body.categories[1].name).toBe('Category 2');
+ });
+
+ it('should handle errors gracefully', async () => {
+ const response = await request(app).get('/product/categories');
+
+ expect(response.status).toBe(200);
+ expect(response.body.status).toBe('success');
+ expect(response.body.categories).toHaveLength(2);
+ });
+});
\ No newline at end of file
diff --git a/src/__test__/searchProduct.test.ts b/src/__test__/searchProduct.test.ts
index f217b26..69199ee 100644
--- a/src/__test__/searchProduct.test.ts
+++ b/src/__test__/searchProduct.test.ts
@@ -116,9 +116,9 @@ describe('Get single product', () => {
.get(`/product/${expiredProductId}`)
.set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`);
- expect(response.status).toBe(400);
- expect(response.body.status).toBe('error');
- expect(response.body.message).toBe('Product expired');
+ expect(response.status).toBe(200);
+ expect(response.body.status).toBe('success');
+ expect(response.body.product).toBeDefined();
});
it('should return 400 for invalid product id', async () => {
diff --git a/src/__test__/vendorProduct.test.ts b/src/__test__/vendorProduct.test.ts
index 54fec95..0ecac5d 100644
--- a/src/__test__/vendorProduct.test.ts
+++ b/src/__test__/vendorProduct.test.ts
@@ -524,13 +524,5 @@ describe('Vendor product management tests', () => {
expect(response.status).toBe(200);
expect(response.body.data.products).toBeUndefined();
});
-
- it('should return an error for invalid input syntax', async () => {
- const response = await request(app)
- .get('/product/all')
- .query({ page: 'invalid', limit: 'limit', category: 'technology' });
-
- expect(response.status).toBe(400);
- });
});
});
diff --git a/src/controllers/productController.ts b/src/controllers/productController.ts
index d24e1e5..662a8a0 100644
--- a/src/controllers/productController.ts
+++ b/src/controllers/productController.ts
@@ -12,6 +12,7 @@ import {
searchProductService,
listAllProductsService,
confirmPayment,
+ getAllCategories
} from '../services';
export const readProduct = async (req: Request, res: Response) => {
@@ -57,4 +58,7 @@ export const searchProduct = async (req: Request, res: Response) => {
};
export const Payment = async (req: Request, res: Response) => {
await confirmPayment(req, res);
+};
+export const getAllCategory = async (req: Request, res: Response) => {
+ await getAllCategories(req, res);
};
\ No newline at end of file
diff --git a/src/entities/Category.ts b/src/entities/Category.ts
index 9152553..5cf1850 100644
--- a/src/entities/Category.ts
+++ b/src/entities/Category.ts
@@ -1,5 +1,6 @@
-import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToMany } from 'typeorm';
import { IsNotEmpty, IsString } from 'class-validator';
+import { Product } from './Product';
@Entity()
export class Category {
@@ -12,6 +13,9 @@ export class Category {
@IsString()
name!: string;
+ @ManyToMany(() => Product, product => product.categories)
+ products!: Product[];
+
@CreateDateColumn()
createdAt!: Date;
diff --git a/src/entities/Product.ts b/src/entities/Product.ts
index ae027ef..ce2ae40 100644
--- a/src/entities/Product.ts
+++ b/src/entities/Product.ts
@@ -80,7 +80,7 @@ export class Product {
@IsBoolean()
isAvailable!: boolean;
- @ManyToMany(() => Category)
+ @ManyToMany(() => Category, category => category.products)
@JoinTable()
categories!: Category[];
diff --git a/src/entities/User.ts b/src/entities/User.ts
index 787c5e0..4808482 100644
--- a/src/entities/User.ts
+++ b/src/entities/User.ts
@@ -25,10 +25,10 @@ export interface UserInterface {
phoneNumber: string;
photoUrl?: string;
verified?: boolean;
+ twoFactorEnabled?: boolean;
status?: 'active' | 'suspended';
userType: 'Admin' | 'Buyer' | 'Vendor';
role?: string;
- twoFactorEnabled?: boolean;
twoFactorCode?: string;
twoFactorCodeExpiresAt?: Date;
createdAt?: Date;
@@ -119,7 +119,7 @@ export class User {
feedbacks!: Feedback[];
@BeforeInsert()
- setRole (): void {
+ setRole(): void {
this.role = this.userType === 'Vendor' ? roles.vendor : roles.buyer;
}
}
\ No newline at end of file
diff --git a/src/index.ts b/src/index.ts
index d689c27..377e419 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -28,7 +28,7 @@ app.use(passport.initialize());
app.use(passport.session());
app.use(express.json());
app.use(cookieParser());
-app.use(cors({ origin: '*' }));
+app.use(cors({ origin: process.env.CLIENT_URL, credentials: true }));
app.use(router);
addDocumentation(app);
app.all('*', (req: Request, res: Response, next) => {
diff --git a/src/routes/ProductRoutes.ts b/src/routes/ProductRoutes.ts
index 49a6c5e..902a1da 100644
--- a/src/routes/ProductRoutes.ts
+++ b/src/routes/ProductRoutes.ts
@@ -18,13 +18,14 @@ import {
createOrder,
getOrders, getOrder,
updateOrder,
- getOrdersHistory,Payment,
+ getOrdersHistory, Payment,
getSingleVendorOrder,
getVendorOrders,
updateVendorOrder,
getBuyerVendorOrders,
getSingleBuyerVendorOrder,
updateBuyerVendorOrder,
+ getAllCategory
} from '../controllers';
const router = Router();
@@ -33,6 +34,7 @@ router.get('/all', listAllProducts);
router.get('/recommended', authMiddleware as RequestHandler, hasRole('BUYER'), getRecommendedProducts);
router.get('/collection', authMiddleware as RequestHandler, hasRole('VENDOR'), readProducts);
router.get('/', authMiddleware as RequestHandler, hasRole('BUYER'), readProducts);
+router.get('/categories', getAllCategory);
router.get('/:id', singleProduct);
router.get('/collection/:id', authMiddleware as RequestHandler, hasRole('VENDOR'), readProduct);
router.post('/', authMiddleware as RequestHandler, hasRole('VENDOR'), upload.array('images', 10), createProduct);
diff --git a/src/routes/UserRoutes.ts b/src/routes/UserRoutes.ts
index ad25a36..20ba2b2 100644
--- a/src/routes/UserRoutes.ts
+++ b/src/routes/UserRoutes.ts
@@ -20,6 +20,10 @@ import { hasRole } from '../middlewares/roleCheck';
import { isTokenValide } from '../middlewares/isValid';
import passport from 'passport';
import '../utils/auth';
+import { start2FAProcess } from '../services/userServices/userStartTwoFactorAuthProcess';
+import { otpTemplate } from '../helper/emailTemplates';
+import { sendOTPEmail } from '../services/userServices/userSendOTPEmail';
+import { sendOTPSMS } from '../services/userServices/userSendOTPMessage';
import { authMiddleware } from '../middlewares/verifyToken';
const router = Router();
@@ -41,29 +45,52 @@ router.get('/google-auth', passport.authenticate('google', { scope: ['profile',
router.get(
'/auth/google/callback',
passport.authenticate('google', {
- successRedirect: '/user/login/success',
- failureRedirect: '/user/login/failed',
+ successRedirect: `${process.env.CLIENT_URL}/login/google-auth`,
+ failureRedirect: `${process.env.CLIENT_URL}/login/google-auth`,
})
);
router.get('/login/success', async (req, res) => {
const user = req.user as UserInterface;
+
if (!user) {
responseError(res, 404, 'user not found');
+ return;
+ }
+
+ if (user.status === 'suspended') {
+ return res.status(400).json({ status: 'error', message: 'Your account has been suspended' });
}
- const payload = {
- id: user?.id,
- email: user?.email,
- role: user?.role,
- };
- const token = jwt.sign(payload, process.env.JWT_SECRET as string, { expiresIn: '24h' });
- res.status(200).json({
+
+ if (!user.twoFactorEnabled) {
+ const payload = {
+ id: user?.id,
+ firstName: user.firstName,
+ lastName: user.lastName,
+ email: user?.email,
+ role: user?.role,
+ };
+ const token = jwt.sign(payload, process.env.JWT_SECRET as string, { expiresIn: '24h' });
+ return res.status(200).json({
+ status: 'success',
+ data: {
+ token: token,
+ message: 'Login success',
+ },
+ });
+ }
+ const otpCode = await start2FAProcess(user.email);
+ const OTPEmailcontent = otpTemplate(user.firstName, otpCode.toString());
+ await sendOTPEmail('Login OTP Code', user.email, OTPEmailcontent);
+ await sendOTPSMS(user.phoneNumber, otpCode.toString());
+ return res.status(200).json({
status: 'success',
data: {
- token: token,
- message: 'Login success',
+ email: user.email,
+ message: 'Please provide the OTP sent to your email or phone',
},
});
});
+
router.get('/login/failed', async (req, res) => {
res.status(401).json({
status: false,
diff --git a/src/services/index.ts b/src/services/index.ts
index f31e750..80d463b 100644
--- a/src/services/index.ts
+++ b/src/services/index.ts
@@ -22,6 +22,7 @@ export * from './productServices/productStatus';
export * from './productServices/viewSingleProduct';
export * from './productServices/searchProduct';
export * from './productServices/payment';
+export * from './productServices/getCategories';
// Buyer wishlist services
export * from './wishListServices/addProduct';
diff --git a/src/services/productServices/getCategories.ts b/src/services/productServices/getCategories.ts
new file mode 100644
index 0000000..04076d8
--- /dev/null
+++ b/src/services/productServices/getCategories.ts
@@ -0,0 +1,24 @@
+import { Request, Response } from 'express';
+import { getRepository } from 'typeorm';
+import { Category } from '../../entities/Category';
+
+export const getAllCategories = async (req: Request, res: Response) => {
+ try {
+ const categoryRepository = getRepository(Category);
+ const categories = await categoryRepository.find({
+ relations: {
+ products: true
+ },
+ select: {
+ products: {
+ id: true
+ }
+ }
+ });
+
+ res.status(200).json({ status: 'success', categories });
+ } catch (error) {
+ console.error('Error fetching categories:', error);
+ res.status(500).json({ status: 'error', message: 'Error fetching categories' });
+ }
+};
diff --git a/src/services/productServices/listAllProductsService.ts b/src/services/productServices/listAllProductsService.ts
index e9fa0ee..da7e601 100644
--- a/src/services/productServices/listAllProductsService.ts
+++ b/src/services/productServices/listAllProductsService.ts
@@ -2,24 +2,17 @@ import { Request, Response } from 'express';
import { Product } from '../../entities/Product';
import { getRepository } from 'typeorm';
import { responseError, responseSuccess } from '../../utils/response.utils';
-import { validate } from 'uuid';
export const listAllProductsService = async (req: Request, res: Response) => {
try {
- const page = req.query.page ? Number(req.query.page) : 1;
- const limit = req.query.limit ? Number(req.query.limit) : 10;
- const skip = (page - 1) * limit;
const category = req.query.category;
const productRepository = getRepository(Product);
const products = await productRepository.find({
- where: {
- categories: {
- name: category as string,
- },
+ where: category ? { categories: { name: category as string } } : {},
+ order: {
+ createdAt: 'DESC',
},
- skip,
- take: limit,
relations: ['categories', 'vendor', 'feedbacks'],
select: {
vendor: {
@@ -33,9 +26,6 @@ export const listAllProductsService = async (req: Request, res: Response) => {
},
});
- if (products.length < 1) {
- return responseSuccess(res, 200, 'No products found');
- }
if (products.length < 1) {
return responseSuccess(res, 200, 'No products found');
}
@@ -44,4 +34,4 @@ export const listAllProductsService = async (req: Request, res: Response) => {
} catch (error) {
responseError(res, 400, (error as Error).message);
}
-};
\ No newline at end of file
+};
diff --git a/src/services/productServices/productStatus.ts b/src/services/productServices/productStatus.ts
index 708621b..eefd8b3 100644
--- a/src/services/productServices/productStatus.ts
+++ b/src/services/productServices/productStatus.ts
@@ -10,7 +10,6 @@ export const productStatusServices = async (req: Request, res: Response) => {
const { id } = req.params;
if (isAvailable === undefined) {
- console.log('Error: Please fill all the required fields');
return responseError(res, 400, 'Please fill all t he required fields');
}
diff --git a/src/services/productServices/searchProduct.ts b/src/services/productServices/searchProduct.ts
index 123672f..e1434b0 100644
--- a/src/services/productServices/searchProduct.ts
+++ b/src/services/productServices/searchProduct.ts
@@ -14,13 +14,14 @@ export const searchProductService = async (req: Request, res: Response) => {
const { name, sortBy, sortOrder, page = 1, limit = 10 }: SearchProductParams = req.query as any;
try {
if (!name) {
- console.log("no name");
return res.status(400).json({ status: 'error', error: 'Please provide a search term' });
}
const productRepository = getRepository(Product);
let query = productRepository.createQueryBuilder('product');
+ query = query.leftJoinAndSelect('product.vendor', 'vendor');
+
query = query.where('LOWER(product.name) LIKE :name', { name: `%${name.toLowerCase()}%` });
if (sortBy && sortOrder) {
diff --git a/src/services/productServices/viewSingleProduct.ts b/src/services/productServices/viewSingleProduct.ts
index 6f49532..f66bb63 100644
--- a/src/services/productServices/viewSingleProduct.ts
+++ b/src/services/productServices/viewSingleProduct.ts
@@ -18,10 +18,6 @@ export const viewSingleProduct = async (req: Request, res: Response) => {
if (!product) {
return res.status(404).send({ status: 'error', message: 'Product not found' });
}
-
- if (product.expirationDate && new Date(product.expirationDate) < new Date()) {
- return res.status(400).json({ status: 'error', message: 'Product expired' });
- }
res.status(200).json({ status: 'success', product: product });
}
} catch (error) {
diff --git a/src/services/userServices/sendResetPasswordLinkService.ts b/src/services/userServices/sendResetPasswordLinkService.ts
index 1d34666..c7d9721 100644
--- a/src/services/userServices/sendResetPasswordLinkService.ts
+++ b/src/services/userServices/sendResetPasswordLinkService.ts
@@ -74,7 +74,7 @@ export const sendPasswordResetLinkService = async (req: Request, res: Response)
password has been generated for you. To reset your password, click the
following link and follow the instructions.
- Reset
Password
diff --git a/src/services/userServices/userLoginService.ts b/src/services/userServices/userLoginService.ts
index 57633d0..fa30bbd 100644
--- a/src/services/userServices/userLoginService.ts
+++ b/src/services/userServices/userLoginService.ts
@@ -42,8 +42,10 @@ export const userLoginService = async (req: Request, res: Response) => {
const token = jwt.sign(
{
id: user.id,
+ firstName: user.firstName,
+ lastName: user.lastName,
email: user.email,
- userType: user.userType,
+ role: user.role,
},
process.env.JWT_SECRET as string,
{ expiresIn: '24h' }
@@ -71,6 +73,7 @@ export const userLoginService = async (req: Request, res: Response) => {
return res.status(200).json({
status: 'success',
data: {
+ email: user.email,
message: 'Please provide the OTP sent to your email or phone',
},
});
diff --git a/src/services/userServices/userProfileUpdateServices.ts b/src/services/userServices/userProfileUpdateServices.ts
index 82fb71d..f65fc81 100644
--- a/src/services/userServices/userProfileUpdateServices.ts
+++ b/src/services/userServices/userProfileUpdateServices.ts
@@ -2,8 +2,6 @@ import { Request, Response } from 'express';
import { responseError, responseSuccess } from '../../utils/response.utils';
import { User, UserInterface } from '../../entities/User';
import { getRepository } from 'typeorm';
-import { userProfileUpdate } from '../../controllers/authController';
-
export const userProfileUpdateServices = async (req: Request, res: Response) => {
try {
diff --git a/src/services/userServices/userRegistrationService.ts b/src/services/userServices/userRegistrationService.ts
index 2d30fe4..c4196b9 100644
--- a/src/services/userServices/userRegistrationService.ts
+++ b/src/services/userServices/userRegistrationService.ts
@@ -50,7 +50,7 @@ export const userRegistrationService = async (req: Request, res: Response) => {
lastName: lastName,
firstName: firstName,
};
- const link = `http://localhost:${process.env.PORT}/user/verify/${user.id}`;
+ const link = `${process.env.CLIENT_URL}/verify-email/${user.id}`;
sendMail(process.env.AUTH_EMAIL, process.env.AUTH_PASSWORD, message, link);
} else {
diff --git a/src/services/userServices/userResendOTP.ts b/src/services/userServices/userResendOTP.ts
index f728b31..204d1d1 100644
--- a/src/services/userServices/userResendOTP.ts
+++ b/src/services/userServices/userResendOTP.ts
@@ -13,7 +13,6 @@ export const userResendOtpService = async (req: Request, res: Response) => {
const { email } = req.body;
if (!email) {
- console.log('No email address provided');
return res.status(400).json({ status: 'error', message: 'Please provide an email' });
}
@@ -21,7 +20,6 @@ export const userResendOtpService = async (req: Request, res: Response) => {
const user = await userRepository.findOneBy({ email });
if (!user) {
- console.log('User not found');
return res.status(404).json({ status: 'error', message: 'Incorrect email' });
}
diff --git a/src/services/userServices/userValidateOTP.ts b/src/services/userServices/userValidateOTP.ts
index c26003f..7dce921 100644
--- a/src/services/userServices/userValidateOTP.ts
+++ b/src/services/userServices/userValidateOTP.ts
@@ -26,8 +26,10 @@ export const userValidateOTP = async (req: Request, res: Response) => {
const token = jwt.sign(
{
id: user?.id,
+ firstName: user?.firstName,
+ lastName: user?.lastName,
email: user?.email,
- userType: user?.userType,
+ role: user?.role,
},
process.env.JWT_SECRET as string,
{ expiresIn: '24h' }
diff --git a/src/utils/auth.ts b/src/utils/auth.ts
index 623883f..f09ca47 100644
--- a/src/utils/auth.ts
+++ b/src/utils/auth.ts
@@ -5,12 +5,13 @@ import { User } from '../entities/User';
import { getRepository } from 'typeorm';
import bcrypt from 'bcrypt';
import '../utils/auth';
+import { v4 as uuid } from 'uuid';
passport.use(
new Strategy(
{
clientID: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
- callbackURL: 'http://localhost:6890/user/auth/google/callback/',
+ callbackURL: `http://localhost:${process.env.PORT || 8000}/user/auth/google/callback/`,
scope: ['email', 'profile'],
},
async (accessToken: any, refreshToken: any, profile: any, cb: any) => {
@@ -27,7 +28,7 @@ passport.use(
return await cb(null, existingUser);
}
const saltRounds = 10;
- const hashedPassword = await bcrypt.hash('password', saltRounds);
+ const hashedPassword = await bcrypt.hash(uuid(), saltRounds);
const newUser = new User();
newUser.firstName = givenName;
newUser.lastName = family_name ?? familyName ?? 'undefined';