Compare commits

..

3 commits

Author SHA1 Message Date
4e1ace0586 sendgrid added 2023-06-10 01:39:09 +03:00
720abc8c02 done with checkout + started mail 2023-06-10 01:20:42 +03:00
0583ba5f51 done with cart 2023-06-10 00:03:43 +03:00
9 changed files with 233 additions and 5 deletions

View file

@ -3,3 +3,4 @@ DB_USERNAME=root
DB_PASSWORD=password DB_PASSWORD=password
JWT_SECRET=your_secret_key JWT_SECRET=your_secret_key
DATABASE_URL="URI" DATABASE_URL="URI"
SENDGRID_API_KEY="API_KEY"

71
package-lock.json generated
View file

@ -9,6 +9,7 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@sendgrid/mail": "^7.7.0",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"dotenv": "^16.1.4", "dotenv": "^16.1.4",
@ -25,6 +26,41 @@
"nodemon": "^2.0.22" "nodemon": "^2.0.22"
} }
}, },
"node_modules/@sendgrid/client": {
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-7.7.0.tgz",
"integrity": "sha512-SxH+y8jeAQSnDavrTD0uGDXYIIkFylCo+eDofVmZLQ0f862nnqbC3Vd1ej6b7Le7lboyzQF6F7Fodv02rYspuA==",
"dependencies": {
"@sendgrid/helpers": "^7.7.0",
"axios": "^0.26.0"
},
"engines": {
"node": "6.* || 8.* || >=10.*"
}
},
"node_modules/@sendgrid/helpers": {
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-7.7.0.tgz",
"integrity": "sha512-3AsAxfN3GDBcXoZ/y1mzAAbKzTtUZ5+ZrHOmWQ279AuaFXUNCh9bPnRpN504bgveTqoW+11IzPg3I0WVgDINpw==",
"dependencies": {
"deepmerge": "^4.2.2"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/@sendgrid/mail": {
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-7.7.0.tgz",
"integrity": "sha512-5+nApPE9wINBvHSUxwOxkkQqM/IAAaBYoP9hw7WwgDNQPxraruVqHizeTitVtKGiqWCKm2mnjh4XGN3fvFLqaw==",
"dependencies": {
"@sendgrid/client": "^7.7.0",
"@sendgrid/helpers": "^7.7.0"
},
"engines": {
"node": "6.* || 8.* || >=10.*"
}
},
"node_modules/@types/bcryptjs": { "node_modules/@types/bcryptjs": {
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz", "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz",
@ -185,6 +221,14 @@
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
}, },
"node_modules/axios": {
"version": "0.26.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
"dependencies": {
"follow-redirects": "^1.14.8"
}
},
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -376,6 +420,14 @@
"ms": "2.0.0" "ms": "2.0.0"
} }
}, },
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/depd": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@ -508,6 +560,25 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/forwarded": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",

View file

@ -10,6 +10,7 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@sendgrid/mail": "^7.7.0",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"dotenv": "^16.1.4", "dotenv": "^16.1.4",

View file

@ -0,0 +1,76 @@
import { Request, Response } from 'express';
import { Cart, ICart, Product, Order, IOrder } from '../mongoose/Schema';
import { sendEmailasync } from '../services/sendGrid';
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 = 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 });
console.log(usersCart)
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 });
}

View file

@ -4,6 +4,10 @@ import { Product, IProduct } from '../mongoose/Schema';
export async function createProduct(req: Request, res: Response) { export async function createProduct(req: Request, res: Response) {
try { try {
const { name, description, price, userId } = req.body; const { name, description, price, userId } = req.body;
if(!name || !description || !price || !userId) {
res.status(400).json({ error: 'Name, description, price and user id are required.' });
return;
}
const product: IProduct = await Product.create({ const product: IProduct = await Product.create({
name, name,
description, description,
@ -32,5 +36,3 @@ export async function listProducts(req: Request, res: Response) {
res.status(500).json({ error: 'An error occurred while listing the products.' }); res.status(500).json({ error: 'An error occurred while listing the products.' });
} }
} }

View file

@ -4,6 +4,7 @@ import cookieParser from 'cookie-parser';
import userRouter from './routes/user'; import userRouter from './routes/user';
import productRouter from './routes/product'; import productRouter from './routes/product';
import cartRouter from './routes/cart';
const env = require('dotenv').config().parsed; const env = require('dotenv').config().parsed;
@ -20,7 +21,10 @@ mongoose.connect(env.DATABASE_URL);
const db = mongoose.connection; const db = mongoose.connection;
// Check for DB connection // Check for DB connection
db.on('error', console.error.bind(console, 'MongoDB connection error:')); db.on('error', () => {
console.error.bind(console, 'MongoDB connection error:')
process.exit(1);
});
db.once('open', () => { db.once('open', () => {
console.log('Connected to MongoDB'); console.log('Connected to MongoDB');
}); });
@ -28,6 +32,7 @@ db.once('open', () => {
// Routes // Routes
app.use('/users', userRouter); app.use('/users', userRouter);
app.use('/products', productRouter); app.use('/products', productRouter);
app.use('/cart', cartRouter);
// Start server // Start server
app.listen(PORT, () => { app.listen(PORT, () => {

View file

@ -19,6 +19,21 @@ export interface IProduct extends Document {
updatedAt: 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({ const UserSchema: Schema = new Schema({
firstName: { type: String, required: true }, firstName: { type: String, required: true },
lastName: { type: String, required: true }, lastName: { type: String, required: true },
@ -38,9 +53,26 @@ const ProductSchema: Schema = new Schema({
updatedAt: { 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 }); ProductSchema.index({ name: 1, userId: 1 }, { unique: true });
const User = mongoose.model<IUser>('User', UserSchema); const User = mongoose.model<IUser>('User', UserSchema);
const Product = mongoose.model<IProduct>('Product', ProductSchema); const Product = mongoose.model<IProduct>('Product', ProductSchema);
const Cart = mongoose.model<ICart>('Cart', CartSchema);
const Order = mongoose.model<IOrder>('Order', OrderSchema);
export { User, Product }; export { User, Product, Cart, Order };

14
src/routes/cart.ts Normal file
View file

@ -0,0 +1,14 @@
import express, { Request } from 'express';
import { authenticateToken } from '../middlewares/checkAuth';
import { addToCart, listCart, checkout } from '../controllers/CartController';
const cartRouter = express.Router();
cartRouter.post('/', authenticateToken, addToCart);
cartRouter.get('/', authenticateToken, listCart);
cartRouter.post('/checkout', authenticateToken, checkout);
export default cartRouter;

26
src/services/sendGrid.ts Normal file
View 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);
}
};