Compare commits
No commits in common. "5723e3516c4c23851f871961dbca940baf7a9d58" and "5505f8e2be879185b2e50ef31ab1880acbf7759d" have entirely different histories.
5723e3516c
...
5505f8e2be
13 changed files with 433 additions and 5 deletions
75
__tests__/controllers/CartController.test.ts
Normal file
75
__tests__/controllers/CartController.test.ts
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { Cart, ICart, Order } from '../mongoose/Schema.test';
|
||||||
|
import { config } from 'dotenv';
|
||||||
|
|
||||||
|
config();
|
||||||
|
|
||||||
|
export async function addToCart(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { userId, productId } = req.body;
|
||||||
|
if (!productId) {
|
||||||
|
res.status(400).json({ error: 'Product id is required.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let cart: ICart | null = await Cart.findOne({ userId });
|
||||||
|
if (!cart) {
|
||||||
|
cart = await Cart.create({
|
||||||
|
userId,
|
||||||
|
products: { [productId]: 1 },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const currentQuantity = cart.products[productId];
|
||||||
|
if (currentQuantity === undefined) {
|
||||||
|
cart.products[productId] = 1;
|
||||||
|
} else {
|
||||||
|
cart.products[productId] += 1;
|
||||||
|
}
|
||||||
|
cart.markModified('products');
|
||||||
|
}
|
||||||
|
|
||||||
|
await cart.save();
|
||||||
|
res.status(200).json(cart);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding product to cart:', error);
|
||||||
|
res.status(500).json({ error: 'An error occurred while adding the product to the cart.' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listCart(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { userId } = req.body;
|
||||||
|
const cart = await Cart.findOne({ userId });
|
||||||
|
if (!cart) {
|
||||||
|
res.status(404).json({ error: 'Cart not found.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(200).json(cart.products);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error listing cart:', error);
|
||||||
|
res.status(500).json({ error: 'An error occurred while listing the cart.' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkout(req: Request, res: Response) {
|
||||||
|
|
||||||
|
const { userId } = req.body;
|
||||||
|
const usersCart = await Cart.findOne({ userId });
|
||||||
|
|
||||||
|
if (!usersCart) {
|
||||||
|
res.status(404).json({ error: 'Cart not found.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const order = await Order.create({
|
||||||
|
userId,
|
||||||
|
products: usersCart.products,
|
||||||
|
});
|
||||||
|
|
||||||
|
await removeCart(userId);
|
||||||
|
// sendEmailasync(order.id, userId);
|
||||||
|
res.status(200).json(order);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeCart(userId: string) {
|
||||||
|
await Cart.deleteOne({ userId });
|
||||||
|
}
|
58
__tests__/controllers/ProductController.test.ts
Normal file
58
__tests__/controllers/ProductController.test.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { Product, IProduct } from '../mongoose/Schema';
|
||||||
|
|
||||||
|
export async function createProduct(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { name, description, price, userId } = req.body;
|
||||||
|
if(!name || !description || !price || !userId) {
|
||||||
|
res.status(400).json({ error: 'Name, description, price are required.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const productExists = await Product.exists({ name, userId });
|
||||||
|
if(productExists) {
|
||||||
|
res.status(400).json({ error: 'Product already exists.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const product: IProduct = await Product.create({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
price,
|
||||||
|
userId: userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json(product);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating product:', error);
|
||||||
|
res.status(500).json({ error: 'An error occurred while creating the product.' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listProducts(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { page, limit } = req.query;
|
||||||
|
const dbPage = Number(page) || 0;
|
||||||
|
const dbLimit = Number(limit) || 50;
|
||||||
|
const products = await Product.find().sort({ price: 1 }).skip(Number(dbPage) * Number(dbLimit)).limit(Number(dbLimit));
|
||||||
|
|
||||||
|
res.json(products);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error listing products:', error);
|
||||||
|
res.status(500).json({ error: 'An error occurred while listing the products.' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProduct(req: Request, res: Response) {
|
||||||
|
const { id } = req.params;
|
||||||
|
try {
|
||||||
|
const product = await Product.findById(id);
|
||||||
|
if(!product) {
|
||||||
|
res.status(404).json({ error: 'Product not found.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json(product);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting product:', error);
|
||||||
|
res.status(404).json({ error: 'Product not found.' });
|
||||||
|
}
|
||||||
|
}
|
87
__tests__/controllers/UserController.test.ts
Normal file
87
__tests__/controllers/UserController.test.ts
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import bcrypt from 'bcryptjs';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import { User, IUser } from '../mongoose/Schema';
|
||||||
|
import { clearJwtCookie, setJwtCookie } from '../middlewares/checkAuth.test';
|
||||||
|
import validate from 'deep-email-validator';
|
||||||
|
|
||||||
|
export async function createUser(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { email, password, address } = req.body;
|
||||||
|
const isValidEmail = await validate(email);
|
||||||
|
if (!isValidEmail.valid) {
|
||||||
|
console.error('Email is invalid:', isValidEmail.validators);
|
||||||
|
return res.status(400).json({ error: 'Email is invalid' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(password && address)) {
|
||||||
|
return res.status(400).json({ error: 'All inputs are required' });
|
||||||
|
}
|
||||||
|
// checkIfUserExists return true if the user exists
|
||||||
|
const userExists = await User.exists({ email });
|
||||||
|
if(userExists) {
|
||||||
|
return res.status(400).json({ error: 'User already exists, Try login :)' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash(password, 10);
|
||||||
|
|
||||||
|
const user: IUser = await User.create({
|
||||||
|
email,
|
||||||
|
password: hashedPassword,
|
||||||
|
address,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
massage: 'User created successfully'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating user:', error);
|
||||||
|
res.status(500).json({ error: 'An error occurred while creating the user.' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function login(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { email, password } = req.body;
|
||||||
|
|
||||||
|
// Check if the user exists
|
||||||
|
const user: IUser | null = await User.findOne({ email });
|
||||||
|
if (!user) {
|
||||||
|
console.error('User not found');
|
||||||
|
return res.status(401).json({ error: 'Invalid email or password' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare the provided password with the stored password
|
||||||
|
const isPasswordCorrect = await bcrypt.compare(password, user.password);
|
||||||
|
if (!isPasswordCorrect) {
|
||||||
|
console.error('Invalid password');
|
||||||
|
return res.status(401).json({ error: 'Invalid email or password' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
userId: user._id
|
||||||
|
}
|
||||||
|
// Generate a JWT
|
||||||
|
const token = jwt.sign(payload, process.env.JWT_SECRET as string, { expiresIn: '1d' });
|
||||||
|
|
||||||
|
setJwtCookie(res, token);
|
||||||
|
|
||||||
|
// Send the JWT as the response
|
||||||
|
res.status(200).json({
|
||||||
|
token
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during login:', error);
|
||||||
|
res.status(500).json({ error: 'An error occurred during login' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function logout(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
clearJwtCookie(res);
|
||||||
|
res.status(200).json({ message: 'Logout successful' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during logout:', error);
|
||||||
|
res.status(500).json({ error: 'An error occurred during logout' });
|
||||||
|
}
|
||||||
|
}
|
39
__tests__/index.test.ts
Normal file
39
__tests__/index.test.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import express from 'express';
|
||||||
|
import mongoose from 'mongoose';
|
||||||
|
import cookieParser from 'cookie-parser';
|
||||||
|
|
||||||
|
import userRouter from './routes/user';
|
||||||
|
import productRouter from './routes/product';
|
||||||
|
import cartRouter from './routes/cart';
|
||||||
|
|
||||||
|
const env = require('dotenv').config().parsed;
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = env.PORT || 3000;
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(cookieParser())
|
||||||
|
|
||||||
|
// Connect to MongoDB using Mongoose
|
||||||
|
mongoose.connect(env.DATABASE_URL);
|
||||||
|
|
||||||
|
const db = mongoose.connection;
|
||||||
|
|
||||||
|
// Check for DB connection
|
||||||
|
db.on('error', () => {
|
||||||
|
console.error.bind(console, 'MongoDB connection error:')
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
db.once('open', () => {
|
||||||
|
console.log('Connected to MongoDB');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
app.use('/users', userRouter);
|
||||||
|
app.use('/products', productRouter);
|
||||||
|
app.use('/cart', cartRouter);
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Server started on port ${PORT}`);
|
||||||
|
});
|
33
__tests__/middlewares/checkAuth.test.ts
Normal file
33
__tests__/middlewares/checkAuth.test.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
|
||||||
|
interface AuthenticatedRequest extends Request {
|
||||||
|
userId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middleware function to authenticate requests
|
||||||
|
export function authenticateToken(req: AuthenticatedRequest, res: Response, next: NextFunction) {
|
||||||
|
|
||||||
|
const token = req.cookies.access_token;
|
||||||
|
if (!token) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt.verify(token, process.env.JWT_SECRET as string, (err: any, decoded: { userId: any; }) => {
|
||||||
|
if (err) {
|
||||||
|
return res.status(401).json({ error: 'In Valid Token' });
|
||||||
|
}
|
||||||
|
req.body.userId = decoded.userId;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set JWT as cookie in the response
|
||||||
|
export function setJwtCookie(res: Response, token: string) {
|
||||||
|
res.cookie('access_token', token, { httpOnly: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear JWT cookie in the response
|
||||||
|
export function clearJwtCookie(res: Response) {
|
||||||
|
res.clearCookie('access_token');
|
||||||
|
}
|
74
__tests__/mongoose/Schema.test.ts
Normal file
74
__tests__/mongoose/Schema.test.ts
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import mongoose, { Schema, Document } from 'mongoose';
|
||||||
|
|
||||||
|
export interface IUser extends Document {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
address: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IProduct extends Document {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
price: number;
|
||||||
|
userId: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICart extends Document {
|
||||||
|
userId: string;
|
||||||
|
products: { [itemId: string]: number };
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IOrder extends Document {
|
||||||
|
userId: string;
|
||||||
|
products: { [itemId: string]: number };
|
||||||
|
emailSent: boolean;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserSchema: Schema = new Schema({
|
||||||
|
email: { type: String, required: true, unique: true },
|
||||||
|
password: { type: String, required: true },
|
||||||
|
address: { type: String, required: true },
|
||||||
|
createdAt: { type: Date, default: Date.now },
|
||||||
|
updatedAt: { type: Date, default: Date.now },
|
||||||
|
});
|
||||||
|
|
||||||
|
const ProductSchema: Schema = new Schema({
|
||||||
|
name: { type: String, required: true },
|
||||||
|
description: { type: String, required: true },
|
||||||
|
price: { type: Number, required: true },
|
||||||
|
userId: { type: Schema.Types.ObjectId, ref: 'User', required: true },
|
||||||
|
createdAt: { type: Date, default: Date.now },
|
||||||
|
updatedAt: { type: Date, default: Date.now },
|
||||||
|
});
|
||||||
|
|
||||||
|
const CartSchema: Schema = new Schema({
|
||||||
|
userId: { type: Schema.Types.ObjectId, ref: 'User', required: true, unique: true },
|
||||||
|
products: { type: Schema.Types.Mixed, default: {} },
|
||||||
|
createdAt: { type: Date, default: Date.now },
|
||||||
|
updatedAt: { type: Date, default: Date.now },
|
||||||
|
});
|
||||||
|
|
||||||
|
const OrderSchema: Schema = new Schema({
|
||||||
|
userId: { type: Schema.Types.ObjectId, ref: 'User', required: true },
|
||||||
|
products: { type: Schema.Types.Mixed, default: {} },
|
||||||
|
emailSent: { type: Boolean, default: false },
|
||||||
|
createdAt: { type: Date, default: Date.now },
|
||||||
|
updatedAt: { type: Date, default: Date.now },
|
||||||
|
});
|
||||||
|
|
||||||
|
ProductSchema.index({ name: 1, userId: 1 }, { unique: true });
|
||||||
|
|
||||||
|
const User = mongoose.model<IUser>('User', UserSchema);
|
||||||
|
const Product = mongoose.model<IProduct>('Product', ProductSchema);
|
||||||
|
const Cart = mongoose.model<ICart>('Cart', CartSchema);
|
||||||
|
const Order = mongoose.model<IOrder>('Order', OrderSchema);
|
||||||
|
|
||||||
|
export { User, Product, Cart, Order };
|
14
__tests__/routes/cart.test.ts
Normal file
14
__tests__/routes/cart.test.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import express from 'express';
|
||||||
|
import { authenticateToken } from '../middlewares/checkAuth.test';
|
||||||
|
import { addToCart, listCart, checkout } from '../controllers/CartController.test';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const cartRouter = express.Router();
|
||||||
|
|
||||||
|
cartRouter.post('/', authenticateToken, addToCart);
|
||||||
|
cartRouter.get('/', authenticateToken, listCart);
|
||||||
|
|
||||||
|
cartRouter.post('/checkout', authenticateToken, checkout);
|
||||||
|
|
||||||
|
export default cartRouter;
|
12
__tests__/routes/product.test.ts
Normal file
12
__tests__/routes/product.test.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import express from 'express';
|
||||||
|
import { authenticateToken } from '../middlewares/checkAuth.test';
|
||||||
|
import { createProduct, listProducts, getProduct } from '../controllers/ProductController.test';
|
||||||
|
|
||||||
|
const productRouter = express.Router();
|
||||||
|
|
||||||
|
productRouter.post('/', authenticateToken, createProduct)
|
||||||
|
productRouter.get('/', listProducts);
|
||||||
|
productRouter.get('/:id', getProduct);
|
||||||
|
|
||||||
|
|
||||||
|
export default productRouter;
|
10
__tests__/routes/user.test.ts
Normal file
10
__tests__/routes/user.test.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import express from 'express';
|
||||||
|
import { createUser, login, logout } from '../controllers/UserController.test';
|
||||||
|
|
||||||
|
const userRouter = express.Router();
|
||||||
|
|
||||||
|
userRouter.post('/', createUser);
|
||||||
|
userRouter.post('/login', login);
|
||||||
|
userRouter.post('/logout', logout);
|
||||||
|
|
||||||
|
export default userRouter;
|
26
__tests__/services/sendGrid.test.ts
Normal file
26
__tests__/services/sendGrid.test.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// import { config } from "dotenv";
|
||||||
|
// import { User, Order } from "../mongoose/Schema";
|
||||||
|
// import client from '@sendgrid/mail';
|
||||||
|
// config();
|
||||||
|
|
||||||
|
// export async function sendEmailasync(orderId:string, userId: string) {
|
||||||
|
// console.log(`Sending email for order ${orderId} to user ${userId}`);
|
||||||
|
// try {
|
||||||
|
// client.setApiKey(process.env.SENDGRID_API_KEY);
|
||||||
|
// const msg = {
|
||||||
|
// to: await User.findOne({ _id: userId }).select('email'),
|
||||||
|
// from: process.env.EMAIL_FROM,
|
||||||
|
// subject: 'Order Confirmation',
|
||||||
|
// text: 'Your order has been placed',
|
||||||
|
// html: '<strong>Thank you for shopping with us!</strong>'
|
||||||
|
// };
|
||||||
|
// await client.send(msg);
|
||||||
|
// console.log(`Email sent for order ${orderId} to user ${userId}`);
|
||||||
|
// // Update the order to indicate email has been sent
|
||||||
|
// await Order.findOne({ _id: orderId }).updateOne({ emailSent: true });
|
||||||
|
// console.log(`Updated order ${orderId} to indicate email has been sent`);
|
||||||
|
// return;
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('Error sending email:', error);
|
||||||
|
// }
|
||||||
|
// };
|
|
@ -27,6 +27,7 @@ export async function addToCart(req: Request, res: Response) {
|
||||||
}
|
}
|
||||||
cart.markModified('products');
|
cart.markModified('products');
|
||||||
}
|
}
|
||||||
|
|
||||||
await cart.save();
|
await cart.save();
|
||||||
res.status(200).json(cart);
|
res.status(200).json(cart);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -18,7 +18,7 @@ export async function createProduct(req: Request, res: Response) {
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
price,
|
price,
|
||||||
userId,
|
userId: userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(201).json(product);
|
res.status(201).json(product);
|
||||||
|
@ -33,7 +33,7 @@ export async function listProducts(req: Request, res: Response) {
|
||||||
const { page, limit } = req.query;
|
const { page, limit } = req.query;
|
||||||
const dbPage = Number(page) || 0;
|
const dbPage = Number(page) || 0;
|
||||||
const dbLimit = Number(limit) || 50;
|
const dbLimit = Number(limit) || 50;
|
||||||
const products = await Product.find(null, 'name description price').sort({ price: 1 }).skip(Number(dbPage) * Number(dbLimit)).limit(Number(dbLimit));
|
const products = await Product.find().sort({ price: 1 }).skip(Number(dbPage) * Number(dbLimit)).limit(Number(dbLimit));
|
||||||
|
|
||||||
res.json(products);
|
res.json(products);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -45,7 +45,7 @@ export async function listProducts(req: Request, res: Response) {
|
||||||
export async function getProduct(req: Request, res: Response) {
|
export async function getProduct(req: Request, res: Response) {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
try {
|
try {
|
||||||
const product = await Product.findById(id, 'name description price');
|
const product = await Product.findById(id);
|
||||||
if(!product) {
|
if(!product) {
|
||||||
res.status(404).json({ error: 'Product not found.' });
|
res.status(404).json({ error: 'Product not found.' });
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
{
|
{
|
||||||
"exclude": ["__tests__"],
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "CommonJS",
|
"module": "CommonJS",
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
|
@ -8,6 +7,6 @@
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src"
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue