This commit is contained in:
Kfir Dayan 2023-06-28 13:48:40 +03:00
commit ecbbf046ce
20 changed files with 2475 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

0
.dockerignore Normal file
View file

6
.env.example Normal file
View file

@ -0,0 +1,6 @@
DB_DATABASE=payplus_db
DB_USERNAME=root
DB_PASSWORD=password
JWT_SECRET=your_secret_key
DATABASE_URL="URI"
SENDGRID_API_KEY="API_KEY"

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
dist
.env
node_modules
init.js

13
Dockerfile Normal file
View file

@ -0,0 +1,13 @@
FROM node:14-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 3000
CMD ["npm", "start"]

251
README.md Normal file
View file

@ -0,0 +1,251 @@
# Ecomm Backend
This repository contains the backend implementation for the Ecomm e-commerce application.
## Description
This is a simple e-commerce backend application built with Node.js, TypeScript, Express.js, MongoDB, and Docker. It provides API endpoints to manage users, products and cart of user.
## My Approach
Creating a simple Express.js application with TypeScript.
added the required dependencies and dev dependencies.
added best ORM to connect with MongoDB and Node.js.
created the required models and controllers for the application.
added the required routes for the application.
added the required environment variables for the application.
added the required middleware for the application.
added the required error handling for the application.
## Table of Contents
- [Technologies Used]
- [How to Run]
- [API Documentation]
- [Users]
- [Create a New User]
- [Login]
- [Products]
- [Get All Products]
## Technologies Used
- Node.js
- TypeScript
- Express.js
- MongoDB
- Mongoose ORM
- Docker (docker-compose)
- bcrypt
- JWT
- deep-email-validator
## How to Run
To run the Ecomm backend application, follow these steps:
1. Clone the repository.
2. Ensure that you have Docker and Docker Compose installed.
3. Implement the required environment variables by creating an `.env` file.
4. Run the following command in the root directory:
```
docker-compose up
```
The application will be running on port 3000, and the database will be running on port 27017.
API Documentation
# Users
Create a New User - POST /users
Creates a new user.
Request Body
```
{
"name": "string",
"email": "string",
"password": "string"
}
```
Response Body
status code 200
```
{
message:
"User created successfully"
}
```
status code 400
```
{
message:
"User already exists"
}
```
status code 500
```
{
message:
"Internal server error"
}
```
Login - POST /users/login
Logs in a user.
Request Body
```
{
"email": "string",
"password": "string"
}
```
Response Body
status code 200
```
{
access-token:
"TOKEN"
}
```
# Products
Get All Products - GET /products
Gets all products.
Parameters
```
page: number(default: 0)
limit: number(default: 50)
```
Response Body
status code 200
```
{
products: [
{
_id: "string",
name: "string",
description: "string",
price: number,
image: "string",
createdAt: "string",
updatedAt: "string"
}
]
}
```
status code 404
```
{
message:
"Product not found."
}
````
# Cart
Add Product to Cart - POST /cart
Adds a product to cart.
(authentication required)
Request Body
```
{
"productId": "string"
}
```
Response Body
status code 200
```
{
Cart :{
"{ProductId}": Quantity
}
}
```
status code 500
```
{
message:
"An error occurred while adding the product to the cart."
}
```
List Cart Items - GET /cart
Lists all cart items.
(authentication required)
Response Body
status code 200
```
{
Cart :{
"{ProductId}": Quantity
}
}
```
status 404
```
{
message:
"Cart not found."
}
```
status code 500
```
{
message:
"An error occurred while listing the cart."
}
```
checkout - POST /cart/checkout
checkout the cart.
(authentication required)
status 200
```
{
order: {
{productId}: quantity
}
}
```
status 404
```
{
message:
"Cart not found."
}
```
# Database Schema
## User
```
{
name: string,
email: string,
password: string,
cart: {
productId: number
}
}
```
## Product
```
{
name: string,
description: string,
price: number
userId: string
}
```
## Cart
```
{
userId: string,
products: Map<string, number>
}
```

19
docker-compose.yaml Normal file
View file

@ -0,0 +1,19 @@
version: '3'
services:
mongodb:
image: arm64v8/mongo:4.0
restart: always
ports:
- 27017:27017
volumes:
- payplus_volume:/data/db
- ./init-scripts/init.js:/docker-entrypoint-initdb.d/mongo-init.js
environment:
- MONGO_INITDB_DATABASE=your-database-name
- MONGO_INITDB_ROOT_USERNAME=your-username
- MONGO_INITDB_ROOT_PASSWORD=your-password
platform: linux/arm64/v8
expose:
- 27017
volumes:
payplus_volume:

1572
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

33
package.json Normal file
View file

@ -0,0 +1,33 @@
{
"name": "payplus",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "nodemon dist/index.js",
"start": "node dist/index.js",
"build": "tsc -p ."
},
"author": "",
"license": "ISC",
"dependencies": {
"@sendgrid/mail": "^7.7.0",
"bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.6",
"deep-email-validator": "^0.1.21",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.0",
"mongodb": "^5.6.0",
"mongoose": "^7.3.1",
"uuid": "^9.0.0"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.2",
"@types/cookie-parser": "^1.4.3",
"@types/express": "^4.17.17",
"@types/jsonwebtoken": "^9.0.2",
"@types/uuid": "^9.0.2",
"nodemon": "^2.0.22"
}
}

View file

@ -0,0 +1,156 @@
import { Request, Response, NextFunction } from 'express';
import { createUser, loginUser } from '../models/userModel';
import { checkIn, checkOut, takeBreak, returnFromBreak, getWork } from '../models/workModel';
import { ApiError } from '../utils/ApiError';
import jwt from 'jsonwebtoken';
import { clearJwtCookie, setJwtCookie } from '../middlewares/checkAuth';
const getUserWork = async (req: Request, res: Response, next: NextFunction) => {
try {
const { userId } = req.body;
const work = await getWork(userId);
if(work instanceof ApiError) {
return next(work);
}
res.status(200).json(work);
} catch {
const error = new ApiError('Error during user creation');
error.statusCode = 500;
error.status = 'fail';
next(error);
}
}
const create = async (req: Request, res: Response, next: NextFunction) => {
try {
const { email, password, firstName, lastName } = req.body;
const user = await createUser({
email,
password,
firstName,
lastName
});
if(user instanceof ApiError) {
return next(user);
}
res.status(201).json(user);
} catch {
const error = new ApiError('Error during user creation');
error.statusCode = 500;
error.status = 'fail';
next(error);
}
}
const login = async (req: Request, res: Response, next: NextFunction) => {
try {
const { email, password } = req.body;
const user: any = await loginUser({
email,
password
});
if(user instanceof ApiError) {
console.log("Error in login")
return res.status(user.statusCode).json({ error: user.message });
}
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 {
const error = new ApiError('Error during user login');
error.statusCode = 500;
error.status = 'fail';
next(error)
}
}
const checkin = async (req: Request, res: Response, next: NextFunction) => {
try {
const { userId } = req.body;
const work = await checkIn(userId);
res.status(201).json(work);
} catch {
const error = new ApiError('Error during user checkin');
error.statusCode = 500;
error.status = 'fail';
next(error);
}
}
const takeBreakHandler = async (req: Request, res: Response, next: NextFunction) => {
console.log("takeBreakHandler")
try {
const { userId } = req.body;
const work = await takeBreak(userId);
if(work instanceof ApiError) {
return next(work);
}
res.status(201).json(work);
} catch {
const error = new ApiError('Error during user take break');
error.statusCode = 500;
error.status = 'fail';
next(error);
}
}
const returnFromBreakHandler = async (req: Request, res: Response, next: NextFunction) => {
try {
const { userId } = req.body;
const work = await returnFromBreak(userId);
if(work instanceof ApiError) {
return next(work);
}
res.status(201).json(work);
} catch {
const error = new ApiError('Error during user return from break');
error.statusCode = 500;
error.status = 'fail';
next(error);
}
}
const checkout = async (req: Request, res: Response, next: NextFunction) => {
try {
const { userId } = req.body;
const work = await checkOut(userId);
res.status(201).json(work);
} catch {
const error = new ApiError('Error during user checkout');
error.statusCode = 500;
error.status = 'fail';
next(error);
}
}
const logout = async (req: Request, res: Response, next: NextFunction) => {
try {
clearJwtCookie(res);
res.status(200).json({ message: 'Logout successful' });
} catch {
const error = new ApiError('Error during user logout');
error.statusCode = 500;
error.status = 'fail';
next(error);
}
}
export {
create,
logout,
login,
checkin,
checkout,
takeBreakHandler,
returnFromBreakHandler,
getUserWork
}

50
src/index.ts Normal file
View file

@ -0,0 +1,50 @@
import express from 'express';
import mongoose from 'mongoose';
import cookieParser from 'cookie-parser';
import userRouter from './routes/userRouter';
import { ApiError } from './utils/ApiError';
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);
// Routes
app.all('*', (req, res, next) => {
const error = new ApiError('Are you lost?');
error.statusCode = 404;
error.status = 'fail';
next(error)
});
// Start server
app.listen(PORT, () => {
console.log(`Server started on port ${PORT}`);
});

View 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');
}

View file

@ -0,0 +1,41 @@
import { Request, Response, NextFunction } from 'express';
import { ApiError } from '../utils/ApiError';
import validate from 'deep-email-validator';
const isValidLogin = async (req: Request, res: Response, next: NextFunction) => {
const user = req.body;
if (!user.email || !user.password) {
const error = new ApiError(`${!user.email ? 'email' : 'password'} is required`);
error.statusCode = 400;
error.status = 'fail';
return next(error);
}
next();
}
const isValidCreateUser = async (req: Request, res: Response, next: NextFunction) => {
const user = req.body;
if (!user.email || !user.password || !user.firstName || !user.lastName ) {
const error = new ApiError(`${!user.email ? 'email' : !user.password ? 'password' : !user.firstName ? 'firstName' : 'lastName'} is required`);
error.statusCode = 400;
error.status = 'fail';
return next(error);
}
const { valid, reason } = await validate(user.email);
if (!valid) {
const error = new ApiError(`Invalid email: ${reason}`);
error.statusCode = 400;
error.status = 'fail';
return next(error);
}
next();
}
export {
isValidLogin,
isValidCreateUser
}

79
src/models/userModel.ts Normal file
View file

@ -0,0 +1,79 @@
import { User } from "../schemas/userSchema";
import { ApiError } from "../utils/ApiError";
import bcrypt from 'bcryptjs';
const createUser = async (user: any) => {
const userExists = await User.exists({ email: user.email });
if (userExists) {
const error = new ApiError('User already exists, Try login :)');
error.statusCode = 400;
error.status = 'fail';
return error;
}
const salt = await bcrypt.genSalt(10);
user.password = await bcrypt.hash(user.password, salt);
const newUser = new User(user);
try {
await newUser.save();
return {
email: newUser.email,
};
} catch (error) {
return error;
}
}
const loginUser = async (user: any) => {
const { email, password } = user;
const userExists = await User.findOne({ email });
if (!userExists) {
const error = new ApiError('Invalid email or password');
error.statusCode = 404;
error.status = 'fail';
return error;
}
const isMatch = await bcrypt.compare(password, userExists.password);
if(!isMatch) {
const error = new ApiError('Invalid email or password');
error.statusCode = 404;
error.status = 'fail';
return error;
}
return userExists;
}
const getAllUsers = async () => {
try {
const users = await User.find();
return users;
} catch {
const error = new ApiError('Error during fetching users');
error.statusCode = 500;
error.status = 'fail';
return error;
}
}
const deleteUser = async (id: string) => {
try {
const user = await User.findByIdAndDelete(id);
return user;
} catch {
const error = new ApiError('Error during user deletion');
error.statusCode = 500;
error.status = 'fail';
return error;
}
}
export {
createUser,
loginUser,
getAllUsers,
deleteUser
}

117
src/models/workModel.ts Normal file
View file

@ -0,0 +1,117 @@
import { Work } from "../schemas/workSchema";
import { ApiError } from "../utils/ApiError";
const checkIn = async (userId: string) => {
const startTime = new Date();
const work = await Work.findOne({ userId, endTime: null });
if (work) {
return work;
}
const newWork = new Work({ userId, startTime, endTime: null, isBreak: false, breakStartTime: null, breakEndTime: null });
return await newWork.save();
}
const checkOut = async (userId: string) => {
const work = await Work.findOne({ userId, endTime: null });
if (!work) {
const error = new ApiError('User is not checked in');
error.statusCode = 400;
error.status = 'fail';
return error;
}
const endTime = new Date();
return await Work.findByIdAndUpdate(work._id, { endTime }, { new: true });
}
const takeBreak = async (userId: string) => {
try {
const work = await Work.findOne({ userId, endTime: null });
if (!work) {
const error = new ApiError('Work not found');
error.statusCode = 404;
error.status = 'fail';
return error;
}
if (work.isBreak) {
const error = new ApiError('Already on a break');
error.statusCode = 400;
error.status = 'fail';
return error;
}
work.isBreak = true;
work.breakStartTime = new Date(); // Set breakStartTime to present 0
await work.save();
return work;
} catch (error) {
const apiError = new ApiError('An error occurred');
apiError.statusCode = 500;
apiError.status = 'error';
return apiError;
}
}
const returnFromBreak = async (userId: string) => {
try {
const work = await Work.findOne({ userId, endTime: null });
if (!work) {
const error = new ApiError('Work not found');
error.statusCode = 404;
error.status = 'fail';
return error;
}
if (!work.isBreak) {
const error = new ApiError('Not on a break');
error.statusCode = 400;
error.status = 'fail';
return error;
}
// Update breakDuration and totalDuration
const breakEndTime = new Date();
const currentTimestamp = breakEndTime.getTime();
const breakDurationInMillis = currentTimestamp - work.breakStartTime.getTime();
const breakDurationInHours = Number((breakDurationInMillis / 3600000).toFixed(3));
work.breakEndTime = breakEndTime;
work.breakDuration = breakDurationInHours + Number(work.breakDuration);
work.isBreak = false;
work.updatedAt = new Date();
await work.save();
return work;
} catch (error) {
const apiError = new ApiError('An error occurred');
apiError.statusCode = 500;
apiError.status = 'error';
return apiError;
}
}
const getWork = async (userId: string) => {
try {
const work = await Work.find({ userId });
if (!work) {
const error = new ApiError('Work not found');
error.statusCode = 404;
error.status = 'fail';
return error;
}
return work;
} catch (error) {
const apiError = new ApiError('An error occurred');
apiError.statusCode = 500;
apiError.status = 'error';
return apiError;
}
}
export {
checkIn,
checkOut,
takeBreak,
returnFromBreak,
getWork
}

21
src/routes/userRouter.ts Normal file
View file

@ -0,0 +1,21 @@
import express from 'express';
import { create, login, logout, checkin, checkout, takeBreakHandler, returnFromBreakHandler, getUserWork } from '../controllers/UserController';
import { isValidCreateUser, isValidLogin } from '../middlewares/usersResourceValidation';
import { authenticateToken } from '../middlewares/checkAuth';
const userRouter = express.Router();
userRouter.post('/', isValidCreateUser, create);
userRouter.post('/login', isValidLogin, login);
userRouter.post('/logout', logout);
userRouter.post('/checkin', authenticateToken, checkin);
userRouter.post('/checkout', authenticateToken, checkout);
userRouter.post('/breaktime', authenticateToken, takeBreakHandler);
userRouter.post('/returnfrombreak', authenticateToken, returnFromBreakHandler);
userRouter.get('/work', authenticateToken, getUserWork);
export default userRouter;

24
src/schemas/userSchema.ts Normal file
View file

@ -0,0 +1,24 @@
import mongoose, { Schema, Document } from 'mongoose';
interface IUser extends Document {
email: string;
password: string;
firstName: string;
lastName: string;
}
const UserSchema: Schema = new Schema({
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
firstName: { type: String, required: true },
lastName: { type: String, required: true },
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now },
});
const User = mongoose.model<IUser>('User', UserSchema);
export {
User,
IUser
}

32
src/schemas/workSchema.ts Normal file
View file

@ -0,0 +1,32 @@
import mongoose, { Schema, Document } from 'mongoose';
interface IWork extends Document {
userId: mongoose.Types.ObjectId;
startTime: Date;
endTime?: Date;
isBreak: boolean;
breakStartTime?: Date;
breakEndTime?: Date;
breakDuration?: Number;
createdAt: Date;
updatedAt: Date;
}
const workSchema: Schema = new Schema({
userId: { type: mongoose.Types.ObjectId, ref: 'User', required: true },
startTime: { type: Date, required: true },
endTime: { type: Date },
isBreak: { type: Boolean, default: false },
breakStartTime: { type: Date },
breakEndTime: { type: Date },
breakDuration: { type: Number, default: 0 },
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now },
});
const Work = mongoose.model<IWork>('Work', workSchema);
export {
Work,
IWork
}

10
src/utils/ApiError.ts Normal file
View file

@ -0,0 +1,10 @@
class ApiError extends Error {
statusCode: number;
status: string;
constructor(message: string) {
super(message);
}
}
export { ApiError };

14
tsconfig.json Normal file
View file

@ -0,0 +1,14 @@
{
"exclude": ["__tests__"],
"compilerOptions": {
"module": "CommonJS",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"target": "es2022",
"moduleResolution": "node",
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"noImplicitAny": true,
}
}