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';