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 9c53a2f
Show file tree
Hide file tree
Showing 20 changed files with 517 additions and 8 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.
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';
56 changes: 56 additions & 0 deletions src/services/productServices/createProduct.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Request, Response } from 'express';
import { Product } from '../../entities/Product';
import { getRepository } from 'typeorm';
import { validateProduct } from '../../helper/productValidator';
import cloudinary from '../../utils/cloudinary';
import { User } from '../../entities/User';

declare module 'express' {
interface Request {
files?: any;
}
}

export const createProductService = async (req: Request, res: Response) => {
const { error } = validateProduct(req.body);
if (error !== undefined) {
return res.status(400).json({ status: 'error', error: error?.details[0].message });
}

const files: any = req.files;

if (files.length < 2) {
return res.status(400).json({ status: 'error', error: 'Please upload more than one image' });
}

const imageUrls: string[] = [];
for (const file of files) {
const image = file.path;
const link = await cloudinary.uploader.upload(image);
imageUrls.push(link.secure_url);
}

const product = new Product();
product.name = req.body.name;
product.description = req.body.description;
product.newPrice = req.body.newPrice;
product.quantity = req.body.quantity;
product.images = imageUrls;

if (req.body.expirationDate) {
product.expirationDate = req.body.expirationDate;
}
product.vendor = req.user as User;

const productRepository = getRepository(Product);
await productRepository.save(product);

product.vendor = product.vendor.id as unknown as User;
return res.status(201).json({
status: 'success',
data: {
message: 'Product created successfully',
product,
},
});
};
32 changes: 32 additions & 0 deletions src/services/productServices/deleteProduct.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Request, Response } from 'express';
import { Product } from '../../entities/Product';
import { getRepository } from 'typeorm';
import { responseError, responseSuccess } from '../../utils/response.utils';


export const deleteProductService = async (req: Request, res: Response) => {
try {
const { id } = req.params;

const productRepository = getRepository(Product);

const product = await productRepository.findOne({
where: {
id: id,
vendor: {
id: req.user?.id
}
}
});

if (product) {
await productRepository.remove(product);
return responseSuccess(res, 200, 'Product successfully deleted');
}

return responseError(res, 404, 'Product not found');

} catch (error) {
responseError(res, 400, (error as Error).message);
}
};
Loading

0 comments on commit 9c53a2f

Please sign in to comment.