Skip to content

Commit

Permalink
Merge pull request #119 from atlp-rwanda/fix-google-auth
Browse files Browse the repository at this point in the history
Fixes the API redirect issue on the same port during Google authentication
  • Loading branch information
faid-terence authored Jun 27, 2024
2 parents 1077386 + db4db9b commit 616c297
Show file tree
Hide file tree
Showing 23 changed files with 143 additions and 58 deletions.
43 changes: 43 additions & 0 deletions src/__test__/categories.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
6 changes: 3 additions & 3 deletions src/__test__/searchProduct.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
8 changes: 0 additions & 8 deletions src/__test__/vendorProduct.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
4 changes: 4 additions & 0 deletions src/controllers/productController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
searchProductService,
listAllProductsService,
confirmPayment,
getAllCategories
} from '../services';

export const readProduct = async (req: Request, res: Response) => {
Expand Down Expand Up @@ -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);
};
6 changes: 5 additions & 1 deletion src/entities/Category.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -12,6 +13,9 @@ export class Category {
@IsString()
name!: string;

@ManyToMany(() => Product, product => product.categories)
products!: Product[];

@CreateDateColumn()
createdAt!: Date;

Expand Down
2 changes: 1 addition & 1 deletion src/entities/Product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export class Product {
@IsBoolean()
isAvailable!: boolean;

@ManyToMany(() => Category)
@ManyToMany(() => Category, category => category.products)
@JoinTable()
categories!: Category[];

Expand Down
4 changes: 2 additions & 2 deletions src/entities/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -119,7 +119,7 @@ export class User {
feedbacks!: Feedback[];

@BeforeInsert()
setRole (): void {
setRole(): void {
this.role = this.userType === 'Vendor' ? roles.vendor : roles.buyer;
}
}
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
4 changes: 3 additions & 1 deletion src/routes/ProductRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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);
Expand Down
49 changes: 38 additions & 11 deletions src/routes/UserRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
24 changes: 24 additions & 0 deletions src/services/productServices/getCategories.ts
Original file line number Diff line number Diff line change
@@ -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' });
}
};
18 changes: 4 additions & 14 deletions src/services/productServices/listAllProductsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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');
}
Expand All @@ -44,4 +34,4 @@ export const listAllProductsService = async (req: Request, res: Response) => {
} catch (error) {
responseError(res, 400, (error as Error).message);
}
};
};
1 change: 0 additions & 1 deletion src/services/productServices/productStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}

Expand Down
3 changes: 2 additions & 1 deletion src/services/productServices/searchProduct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 0 additions & 4 deletions src/services/productServices/viewSingleProduct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion src/services/userServices/sendResetPasswordLinkService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
</p>
<a href="${process.env.FRONTEND_URL}/${process.env.PASSWORD_ROUTE}?userid=${existingUser.id}&email=${existingUser.email}" target="_blank"
<a href="${process.env.CLIENT_URL}/reset-password?userid=${existingUser.id}&email=${existingUser.email}" target="_blank"
style="background:#20e277;text-decoration:none !important; font-weight:500; margin-top:35px; color:#fff;text-transform:uppercase; font-size:14px;padding:10px 24px;display:inline-block;border-radius:50px;">Reset
Password</a>
</td>
Expand Down
5 changes: 4 additions & 1 deletion src/services/userServices/userLoginService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' }
Expand Down Expand Up @@ -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',
},
});
Expand Down
2 changes: 0 additions & 2 deletions src/services/userServices/userProfileUpdateServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 616c297

Please sign in to comment.