done
This commit is contained in:
commit
ecbbf046ce
20 changed files with 2475 additions and 0 deletions
BIN
.DS_Store
vendored
Normal file
BIN
.DS_Store
vendored
Normal file
Binary file not shown.
0
.dockerignore
Normal file
0
.dockerignore
Normal file
6
.env.example
Normal file
6
.env.example
Normal 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
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
dist
|
||||||
|
.env
|
||||||
|
node_modules
|
||||||
|
init.js
|
13
Dockerfile
Normal file
13
Dockerfile
Normal 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
251
README.md
Normal 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
19
docker-compose.yaml
Normal 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
1572
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
33
package.json
Normal file
33
package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
156
src/controllers/UserController.ts
Normal file
156
src/controllers/UserController.ts
Normal 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
50
src/index.ts
Normal 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}`);
|
||||||
|
});
|
33
src/middlewares/checkAuth.ts
Normal file
33
src/middlewares/checkAuth.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');
|
||||||
|
}
|
41
src/middlewares/usersResourceValidation.ts
Normal file
41
src/middlewares/usersResourceValidation.ts
Normal 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
79
src/models/userModel.ts
Normal 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
117
src/models/workModel.ts
Normal 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
21
src/routes/userRouter.ts
Normal 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
24
src/schemas/userSchema.ts
Normal 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
32
src/schemas/workSchema.ts
Normal 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
10
src/utils/ApiError.ts
Normal 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
14
tsconfig.json
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue