Skip to content

Commit

Permalink
feat(product managment) add product managment this commit adds produc…
Browse files Browse the repository at this point in the history
…t entity with its relation of a vendor. it is used to implement product managment for vendor. Resolves: #48
  • Loading branch information
MC-Knight authored and aimedivin committed May 9, 2024
1 parent a8de772 commit dbe54ff
Show file tree
Hide file tree
Showing 26 changed files with 537 additions and 14 deletions.
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ PINDO_API_URL = ********************************
PINDO_SENDER = ********************************
JWT_SECRET = ********************************
TWO_FA_MINS = ********************************

HOST = *******************
AUTH_EMAIL = *********************
AUTH_PASSWORD = ******************
AUTH_PASSWORD = ******************

CLOUDNARY_API_KEY = **************
CLOUDINARY_CLOUD_NAME = **************
CLOUDINARY_API_SECRET = **************
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,27 @@
"dependencies": {
"@types/express-winston": "^4.0.0",
"@types/jsonwebtoken": "^9.0.6",
"@types/multer": "^1.4.11",
"@types/nodemailer": "^6.4.14",
"@types/swagger-jsdoc": "^6.0.4",
"@types/swagger-ui-express": "^4.1.6",
"axios": "^1.6.8",
"bcrypt": "^5.1.1",
"class-validator": "^0.14.1",
"cloudinary": "^2.2.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"cross-env": "^7.0.3",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-winston": "^4.2.0",
"highlight.js": "^11.9.0",
"joi": "^17.13.1",
"jsend": "^1.1.0",
"jsonwebtoken": "^9.0.2",
"mailgen": "^2.0.28",
"morgan": "^1.10.0",
"multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.13",
"nodemon": "^3.1.0",
"pg": "^8.11.5",
Expand Down
Empty file added src/@types/index.d.ts
Empty file.
7 changes: 5 additions & 2 deletions src/__test__/isAllowed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,11 @@ afterAll(async () => {
const connection = getConnection();
const userRepository = connection.getRepository(User);

// Delete all records from the User
await userRepository.delete({});

// Close the connection to the test database
await connection.close();
await connection.close();
});

describe('Middleware - checkUserStatus', () => {
Expand All @@ -63,7 +66,7 @@ describe('Middleware - checkUserStatus', () => {
};
nextMock = jest.fn();
});

it('should return 401 if user is not authenticated', async () => {
await checkUserStatus(reqMock as Request, resMock as Response, nextMock);
expect(responseError).toHaveBeenCalledWith(resMock, 401, 'Authentication required');
Expand Down
10 changes: 9 additions & 1 deletion src/__test__/logout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,15 @@ beforeAll(async () => {
});

afterAll(async () => {
await getConnection('testConnection').close();
const connection = getConnection('testConnection');
const userRepository = connection.getRepository(User);

// Delete all records from the User
await userRepository.delete({});

// Close the connection to the test database
await connection.close();

server.close();
});

Expand Down
2 changes: 1 addition & 1 deletion src/__test__/roleCheck.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ afterAll(async () => {
const userRepository = connection.getRepository(User);

// Delete all records from the User
await userRepository.clear();
await userRepository.delete({});

// Close the connection to the test database
await connection.close();
Expand Down
2 changes: 1 addition & 1 deletion src/__test__/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ afterAll(async () => {
const userRepository = connection.getRepository(User);

// Delete all records from the User
await userRepository.clear();
await userRepository.delete({});

// Close the connection to the test database
await connection.close();
Expand Down
2 changes: 1 addition & 1 deletion src/__test__/userServices.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ afterAll(async () => {
const userRepository = connection.getRepository(User);

// Delete all records from the User
await userRepository.clear();
await userRepository.delete({});

// Close the connection to the test database
await connection.close();
Expand Down
3 changes: 3 additions & 0 deletions src/__test__/userStatus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ afterAll(async () => {
const connection = getConnection();
const userRepository = connection.getRepository(User);

// Delete all records from the User
await userRepository.delete({});

// Close the connection to the test database
await connection.close();
server.close();
Expand Down
26 changes: 26 additions & 0 deletions src/controllers/productController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Request, Response } from 'express';
import { createProductService, updateProductService, removeProductImageService, readProductService, readProductsService, deleteProductService } from '../services';

export const readProduct = async (req: Request, res: Response) => {
await readProductService(req, res);
};

export const readProducts = async (req: Request, res: Response) => {
await readProductsService(req, res);
};

export const createProduct = async (req: Request, res: Response) => {
await createProductService(req, res);
};

export const updateProduct = async (req: Request, res: Response) => {
await updateProductService(req, res);
};

export const removeProductImage = async (req: Request, res: Response) => {
await removeProductImageService(req, res);
};

export const deleteProduct = async (req: Request, res: Response) => {
await deleteProductService(req, res);
};
54 changes: 54 additions & 0 deletions src/entities/Product.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Entity, PrimaryGeneratedColumn, Column, Unique, ManyToOne, CreateDateColumn, UpdateDateColumn } from 'typeorm';
import { IsNotEmpty, IsString, IsBoolean, ArrayNotEmpty, IsArray, MaxLength } from 'class-validator';
import { User } from './User';

@Entity()
@Unique(['id'])
export class Product {
@PrimaryGeneratedColumn('uuid')
@IsNotEmpty()
id!: string;

@ManyToOne(() => User)
@IsNotEmpty()
vendor!: User;

@Column()
@IsNotEmpty()
@IsString()
name!: string;

@Column()
@IsNotEmpty()
description!: string;

@Column('simple-array')
@IsArray()
@ArrayNotEmpty()
@MaxLength(10)
images!: string[];

@Column('decimal')
@IsNotEmpty()
newPrice!: number;

@Column('decimal', { nullable: true })
oldPrice?: number;

@Column('timestamp', { nullable: true })
expirationDate?: Date;

@Column('int')
@IsNotEmpty()
quantity!: number;

@Column({ default: true })
@IsBoolean()
isAvailable!: boolean;

@CreateDateColumn()
createdAt!: Date;

@UpdateDateColumn()
updatedAt!: Date;
}
25 changes: 25 additions & 0 deletions src/helper/productValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Joi from 'joi';
import { Product } from '../lib/types';

export const validateProduct = (
product: Pick<Product, 'name' | 'description' | 'newPrice' | 'quantity' | 'expirationDate'>
): Joi.ValidationResult<any> => {
const schema = Joi.object({
name: Joi.string().min(3).required().messages({
'any.required': 'name is required.',
}),
description: Joi.string().min(3).required().messages({
'any.required': 'description is required.',
}),
newPrice: Joi.number().required().messages({
'any.required': 'newPrice is required.',
}),
quantity: Joi.number().required().messages({
'any.required': 'quantity is required.',
}),
expirationDate: Joi.date(),
oldPrice: Joi.number(),
});

return schema.validate(product);
};
16 changes: 16 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { User } from '../entities/User';

export type Product = {
id: string;
vendor: User;
name: string;
description: string;
images: string[];
newPrice: number;
oldPrice?: number;
expirationDate?: Date;
quantity: number;
isAvailable: boolean;
createdAt: Date;
updatedAt: Date;
};
14 changes: 14 additions & 0 deletions src/middlewares/multer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import multer from 'multer';
import path from 'path';

export default multer({
storage: multer.diskStorage({}),
fileFilter: (req, file, next) => {
const ext = path.extname(file.originalname);
const supported = ['.png', '.jpg', '.jpeg', '.webp'];
if (!supported.includes(ext)) {
next(new Error(`file type not supported\ntry ${supported} are supported`));
}
next(null, true);
},
});
50 changes: 50 additions & 0 deletions src/middlewares/verifyToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Request, Response, NextFunction } from 'express';
import { User, UserInterface } from '../entities/User';
import { getRepository } from 'typeorm';
import jwt, { type JwtPayload, type Secret } from 'jsonwebtoken';
import dotenv from 'dotenv';

dotenv.config();

interface AuthRequest extends Request {
user?: UserInterface;
}

export const authMiddleware = async (req: AuthRequest, res: Response, next: NextFunction) => {
const authHeader = req.headers.authorization;

if (authHeader === undefined) {
return res.status(401).json({ error: 'Access denied. No token provided.' });
}

const [bearer, token] = authHeader.split(' ');

if (bearer !== 'Bearer' || token === undefined) {
return res.status(401).json({ error: 'Please login' });
}

if (token !== undefined) {
try {
jwt.verify(token, process.env.JWT_SECRET as Secret, async (err, decodedToken) => {
if (err !== null) {
return res.status(403).json({ status: 'error', error: 'Access denied' });
}

if (decodedToken !== undefined) {
const { email } = decodedToken as JwtPayload;
const userRepository = getRepository(User);
const user = await userRepository.findOneBy({ email });

if (!user) {
return res.status(401).json({ status: 'error', error: 'You are not Authorized' });
}

req.user = user as UserInterface;
next();
}
});
} catch (error) {
return res.status(401).json({ error: 'Invalid token' });
}
}
};
17 changes: 17 additions & 0 deletions src/routes/ProductRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Router } from 'express';

import upload from '../middlewares/multer';
import { authMiddleware } from '../middlewares/verifyToken';
import { hasRole } from '../middlewares';
import { createProduct, updateProduct, removeProductImage, readProducts, readProduct, deleteProduct } from '../controllers/productController';

const router = Router();

router.get('/', authMiddleware, hasRole('VENDOR'), readProducts);
router.get('/:id', authMiddleware, hasRole('VENDOR'), readProduct);
router.post('/', authMiddleware, hasRole('VENDOR'), upload.array('images', 10), createProduct);
router.put('/:id', authMiddleware, hasRole('VENDOR'), upload.array('images', 10), updateProduct);
router.delete('/images/:id', authMiddleware, hasRole('VENDOR'), removeProductImage);
router.delete('/:id', authMiddleware, hasRole('VENDOR'), deleteProduct);

export default router;
2 changes: 2 additions & 0 deletions src/routes/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Request, Response, Router } from 'express';
import { responseSuccess } from '../utils/response.utils';
import userRoutes from './UserRoutes';
import productRoutes from './ProductRoutes';

const router = Router();

Expand All @@ -9,5 +10,6 @@ router.get('/api/v1/status', (req: Request, res: Response) => {
});

router.use('/user', userRoutes);
router.use('/product', productRoutes);

export default router;
16 changes: 11 additions & 5 deletions src/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@

export * from "./userServices/sendResetPasswordLinkService";
export * from "./userServices/userPasswordResetService";
export * from "./userServices/userRegistrationService";
export * from "./userServices/userValidationService";
export * from './userServices/sendResetPasswordLinkService';
export * from './userServices/userPasswordResetService';
export * from './userServices/userRegistrationService';
export * from './userServices/userValidationService';
export * from './userServices/userEnableTwoFactorAuth';
export * from './userServices/userDisableTwoFactorAuth';
export * from './userServices/userValidateOTP';
export * from './userServices/userLoginService';
export * from './userServices/userResendOTP';
export * from './userServices/logoutServices';

// Vendor product services
export * from './productServices/createProduct';
export * from './productServices/updateProduct';
export * from './productServices/removeProductImage';
export * from './productServices/readProduct';
export * from './productServices/deleteProduct';
Loading

0 comments on commit dbe54ff

Please sign in to comment.