├── src ├── types │ └── index.ts ├── user │ ├── userTypes.ts │ ├── userRouter.ts │ ├── userModel.ts │ └── userController.ts ├── book │ ├── bookTypes.ts │ ├── bookModel.ts │ ├── bookRouter.ts │ └── bookController.ts ├── config │ ├── cloudinary.ts │ ├── config.ts │ └── db.ts ├── middlewares │ ├── globalErrorHandler.ts │ └── authenticate.ts └── app.ts ├── .prettierrc.json ├── .env.example ├── server.ts ├── ecosystem.config.js ├── .eslintrc.json ├── Dockerfile ├── docker-compose.yaml ├── package.json ├── .gitignore └── tsconfig.json /src/types/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4 3 | } -------------------------------------------------------------------------------- /src/user/userTypes.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | _id: string; 3 | name: string; 4 | email: string; 5 | password: string; 6 | } 7 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PORT= 2 | MONGO_CONNECTION_STRING= 3 | NODE_ENV= 4 | JWT_SECRET= 5 | CLOUDINARY_CLOUD= 6 | CLOUDINARY_API_KEY= 7 | CLOUDINARY_API_SECRET= 8 | FRONTEND_DOMAIN= -------------------------------------------------------------------------------- /src/book/bookTypes.ts: -------------------------------------------------------------------------------- 1 | import { User } from "../user/userTypes"; 2 | 3 | export interface Book { 4 | _id: string; 5 | title: string; 6 | description: string; 7 | author: User; 8 | genre: string; 9 | coverImage: string; 10 | file: string; 11 | createdAt: Date; 12 | updatedAt: Date; 13 | } 14 | -------------------------------------------------------------------------------- /src/config/cloudinary.ts: -------------------------------------------------------------------------------- 1 | import { v2 as cloudinary } from "cloudinary"; 2 | import { config } from "./config"; 3 | 4 | cloudinary.config({ 5 | cloud_name: config.cloudinaryCloud, 6 | api_key: config.cloudinaryApiKey, 7 | api_secret: config.cloudinarySecret, 8 | }); 9 | 10 | export default cloudinary; 11 | -------------------------------------------------------------------------------- /src/user/userRouter.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { createUser, loginUser } from "./userController"; 3 | 4 | const userRouter = express.Router(); 5 | 6 | // routes 7 | userRouter.post("/register", createUser); 8 | userRouter.post("/login", loginUser); 9 | 10 | export default userRouter; 11 | -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | import app from "./src/app"; 2 | import { config } from "./src/config/config"; 3 | import connectDB from "./src/config/db"; 4 | 5 | const startServer = async () => { 6 | // Connect database 7 | await connectDB(); 8 | 9 | const port = config.port || 3000; 10 | 11 | app.listen(port, () => { 12 | console.log(`Listening on port: ${port}`); 13 | }); 14 | }; 15 | 16 | startServer(); 17 | -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: "elib-backend-app", 5 | script: "./dist/server.js", 6 | instances: "max", 7 | exec_mode: "cluster", 8 | env: { 9 | NODE_ENV: "development", 10 | }, 11 | env_production: { 12 | NODE_ENV: "production", 13 | }, 14 | }, 15 | ], 16 | }; 17 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaVersion": "latest", 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "@typescript-eslint" 17 | ], 18 | "rules": { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/user/userModel.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import { User } from "./userTypes"; 3 | 4 | const userSchema = new mongoose.Schema( 5 | { 6 | name: { 7 | type: String, 8 | required: true, 9 | }, 10 | email: { 11 | type: String, 12 | unique: true, 13 | required: true, 14 | }, 15 | password: { 16 | type: String, 17 | required: true, 18 | }, 19 | }, 20 | { timestamps: true } 21 | ); 22 | 23 | export default mongoose.model("User", userSchema); 24 | -------------------------------------------------------------------------------- /src/config/config.ts: -------------------------------------------------------------------------------- 1 | import { config as conf } from "dotenv"; 2 | conf(); 3 | 4 | const _config = { 5 | port: process.env.PORT, 6 | databaseUrl: process.env.MONGO_CONNECTION_STRING, 7 | env: process.env.NODE_ENV, 8 | jwtSecret: process.env.JWT_SECRET, 9 | cloudinaryCloud: process.env.CLOUDINARY_CLOUD, 10 | cloudinaryApiKey: process.env.CLOUDINARY_API_KEY, 11 | cloudinarySecret: process.env.CLOUDINARY_API_SECRET, 12 | frontendDomain: process.env.FRONTEND_DOMAIN, 13 | }; 14 | 15 | export const config = Object.freeze(_config); 16 | -------------------------------------------------------------------------------- /src/middlewares/globalErrorHandler.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import { HttpError } from "http-errors"; 3 | import { config } from "../config/config"; 4 | 5 | const globalErrorHandler = ( 6 | err: HttpError, 7 | req: Request, 8 | res: Response, 9 | next: NextFunction 10 | ) => { 11 | const statusCode = err.statusCode || 500; 12 | 13 | return res.status(statusCode).json({ 14 | message: err.message, 15 | errorStack: config.env === "development" ? err.stack : "", 16 | }); 17 | }; 18 | 19 | export default globalErrorHandler; 20 | -------------------------------------------------------------------------------- /src/config/db.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import { config } from "./config"; 3 | 4 | const connectDB = async () => { 5 | try { 6 | mongoose.connection.on("connected", () => { 7 | console.log("Connected to database successfully"); 8 | }); 9 | 10 | mongoose.connection.on("error", (err) => { 11 | console.log("Error in connecting to database.", err); 12 | }); 13 | 14 | await mongoose.connect(config.databaseUrl as string); 15 | } catch (err) { 16 | console.error("Failed to connect to database.", err); 17 | process.exit(1); 18 | } 19 | }; 20 | 21 | export default connectDB; 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine as builder 2 | 3 | WORKDIR /app 4 | 5 | COPY package*.json ./ 6 | 7 | RUN npm ci 8 | 9 | COPY . . 10 | 11 | RUN npm run build 12 | 13 | # build production image 14 | 15 | FROM node:18-alpine 16 | 17 | WORKDIR /app 18 | 19 | COPY package*.json ./ 20 | 21 | ENV NODE_ENV=production 22 | 23 | RUN npm ci 24 | #--omit=dev 25 | 26 | COPY --from=builder /app/dist ./dist 27 | 28 | RUN chown -R node:node /app && chmod -R 755 /app 29 | 30 | RUN npm install pm2 -g 31 | 32 | COPY ecosystem.config.js . 33 | 34 | USER node 35 | 36 | EXPOSE 5513 37 | 38 | CMD ["pm2-runtime", "start", "ecosystem.config.js"] 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | mongosever: 3 | image: mongo 4 | container_name: mongodb-server 5 | environment: 6 | - MONGO_INITDB_ROOT_USERNAME=root 7 | - MONGO_INITDB_ROOT_PASSWORD=root 8 | volumes: 9 | - ~/mongo/data:/data/db 10 | networks: 11 | - elib-network 12 | 13 | backendapi: 14 | image: codersgyan/elib-backend:v2 15 | container_name: elib-backend-server 16 | ports: 17 | - "5513:5513" 18 | networks: 19 | - elib-network 20 | env_file: .env 21 | depends_on: 22 | - mongosever 23 | 24 | networks: 25 | elib-network: 26 | driver: bridge 27 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import express, { NextFunction, Request, Response } from "express"; 2 | import cors from "cors"; 3 | import globalErrorHandler from "./middlewares/globalErrorHandler"; 4 | import userRouter from "./user/userRouter"; 5 | import bookRouter from "./book/bookRouter"; 6 | import { config } from "./config/config"; 7 | 8 | const app = express(); 9 | 10 | app.use( 11 | cors({ 12 | origin: config.frontendDomain, 13 | }) 14 | ); 15 | 16 | app.use(express.json()); 17 | 18 | // Routes 19 | // Http methods: GET, POST, PUT, PATCH, DELETE 20 | app.get("/", (req, res, next) => { 21 | res.json({ message: "Welcome to elib apis" }); 22 | }); 23 | 24 | app.use("/api/users", userRouter); 25 | app.use("/api/books", bookRouter); 26 | 27 | // Global error handler 28 | app.use(globalErrorHandler); 29 | 30 | export default app; 31 | -------------------------------------------------------------------------------- /src/middlewares/authenticate.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | import createHttpError from "http-errors"; 3 | import { verify } from "jsonwebtoken"; 4 | import { config } from "../config/config"; 5 | 6 | export interface AuthRequest extends Request { 7 | userId: string; 8 | } 9 | const authenticate = (req: Request, res: Response, next: NextFunction) => { 10 | const token = req.header("Authorization"); 11 | if (!token) { 12 | return next(createHttpError(401, "Authorization token is required.")); 13 | } 14 | 15 | try { 16 | const parsedToken = token.split(" ")[1]; 17 | const decoded = verify(parsedToken, config.jwtSecret as string); 18 | const _req = req as AuthRequest; 19 | _req.userId = decoded.sub as string; 20 | 21 | next(); 22 | } catch (err) { 23 | return next(createHttpError(401, "Token expired.")); 24 | } 25 | }; 26 | 27 | export default authenticate; 28 | -------------------------------------------------------------------------------- /src/book/bookModel.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import { Book } from "./bookTypes"; 3 | 4 | const bookSchema = new mongoose.Schema( 5 | { 6 | title: { 7 | type: String, 8 | required: true, 9 | }, 10 | description: { 11 | type: String, 12 | require: true, 13 | }, 14 | author: { 15 | type: mongoose.Schema.Types.ObjectId, 16 | // add ref 17 | ref: "User", 18 | required: true, 19 | }, 20 | coverImage: { 21 | type: String, 22 | required: true, 23 | }, 24 | file: { 25 | type: String, 26 | requied: true, 27 | }, 28 | genre: { 29 | type: String, 30 | required: true, 31 | }, 32 | }, 33 | { timestamps: true } 34 | ); 35 | 36 | export default mongoose.model("Book", bookSchema); 37 | -------------------------------------------------------------------------------- /src/book/bookRouter.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import express from "express"; 3 | import { 4 | createBook, 5 | deleteBook, 6 | getSingleBook, 7 | listBooks, 8 | updateBook, 9 | } from "./bookController"; 10 | import multer from "multer"; 11 | import authenticate from "../middlewares/authenticate"; 12 | 13 | const bookRouter = express.Router(); 14 | 15 | // file store local -> 16 | const upload = multer({ 17 | dest: path.resolve(__dirname, "../../public/data/uploads"), 18 | // todo: put limit 10mb max. 19 | limits: { fileSize: 3e7 }, // 30mb 30 * 1024 * 1024 20 | }); 21 | // routes 22 | // /api/books 23 | bookRouter.post( 24 | "/", 25 | authenticate, 26 | upload.fields([ 27 | { name: "coverImage", maxCount: 1 }, 28 | { name: "file", maxCount: 1 }, 29 | ]), 30 | createBook 31 | ); 32 | 33 | bookRouter.patch( 34 | "/:bookId", 35 | authenticate, 36 | upload.fields([ 37 | { name: "coverImage", maxCount: 1 }, 38 | { name: "file", maxCount: 1 }, 39 | ]), 40 | updateBook 41 | ); 42 | 43 | bookRouter.get("/", listBooks); 44 | bookRouter.get("/:bookId", getSingleBook); 45 | 46 | bookRouter.delete("/:bookId", authenticate, deleteBook); 47 | 48 | export default bookRouter; 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elib", 3 | "version": "1.0.0", 4 | "description": "This is a ebook api project", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "nodemon server.ts", 8 | "build": "tsc", 9 | "docker:publish": "docker build -t codersgyan/elib-backend:v2 --platform linux/amd64 . && docker push codersgyan/elib-backend:v2" 10 | }, 11 | "keywords": [ 12 | "rest", 13 | "api", 14 | "express" 15 | ], 16 | "author": "Rakesh K ", 17 | "license": "ISC", 18 | "devDependencies": { 19 | "@types/bcrypt": "^5.0.2", 20 | "@types/dotenv": "^8.2.0", 21 | "@types/express": "^4.17.21", 22 | "@types/http-errors": "^2.0.4", 23 | "@types/jsonwebtoken": "^9.0.6", 24 | "@types/mongoose": "^5.11.97", 25 | "@types/multer": "^1.4.11", 26 | "@types/node": "^20.12.5", 27 | "nodemon": "^3.1.0", 28 | "ts-node": "^10.9.2", 29 | "typescript": "^5.4.4" 30 | }, 31 | "dependencies": { 32 | "@types/cors": "^2.8.17", 33 | "bcrypt": "^5.1.1", 34 | "cloudinary": "^2.1.0", 35 | "cors": "^2.8.5", 36 | "dotenv": "^16.4.5", 37 | "express": "^4.19.2", 38 | "http-errors": "^2.0.0", 39 | "jsonwebtoken": "^9.0.2", 40 | "mongoose": "^8.3.0", 41 | "multer": "^1.4.5-lts.1" 42 | } 43 | } -------------------------------------------------------------------------------- /src/user/userController.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | import createHttpError from "http-errors"; 3 | import bcrypt from "bcrypt"; 4 | import userModel from "./userModel"; 5 | import { sign } from "jsonwebtoken"; 6 | import { config } from "../config/config"; 7 | import { User } from "./userTypes"; 8 | 9 | const createUser = async (req: Request, res: Response, next: NextFunction) => { 10 | const { name, email, password } = req.body; 11 | 12 | // Validation 13 | if (!name || !email || !password) { 14 | const error = createHttpError(400, "All fields are required"); 15 | return next(error); 16 | } 17 | 18 | // Database call. 19 | try { 20 | const user = await userModel.findOne({ email }); 21 | if (user) { 22 | const error = createHttpError( 23 | 400, 24 | "User already exists with this email." 25 | ); 26 | return next(error); 27 | } 28 | } catch (err) { 29 | return next(createHttpError(500, "Error while getting user")); 30 | } 31 | 32 | /// password -> hash 33 | 34 | const hashedPassword = await bcrypt.hash(password, 10); 35 | 36 | let newUser: User; 37 | try { 38 | newUser = await userModel.create({ 39 | name, 40 | email, 41 | password: hashedPassword, 42 | }); 43 | } catch (err) { 44 | return next(createHttpError(500, "Error while creating user.")); 45 | } 46 | 47 | try { 48 | // Token generation JWT 49 | const token = sign({ sub: newUser._id }, config.jwtSecret as string, { 50 | expiresIn: "7d", 51 | algorithm: "HS256", 52 | }); 53 | // Response 54 | res.status(201).json({ accessToken: token }); 55 | } catch (err) { 56 | return next(createHttpError(500, "Error while signing the jwt token")); 57 | } 58 | }; 59 | 60 | const loginUser = async (req: Request, res: Response, next: NextFunction) => { 61 | const { email, password } = req.body; 62 | 63 | if (!email || !password) { 64 | return next(createHttpError(400, "All fields are required")); 65 | } 66 | 67 | // todo: wrap in try catch. 68 | const user = await userModel.findOne({ email }); 69 | if (!user) { 70 | return next(createHttpError(404, "User not found.")); 71 | } 72 | 73 | const isMatch = await bcrypt.compare(password, user.password); 74 | 75 | if (!isMatch) { 76 | return next(createHttpError(400, "Username or password incorrect!")); 77 | } 78 | 79 | // todo: handle errors 80 | // Create accesstoken 81 | const token = sign({ sub: user._id }, config.jwtSecret as string, { 82 | expiresIn: "7d", 83 | algorithm: "HS256", 84 | }); 85 | 86 | res.json({ accessToken: token }); 87 | }; 88 | 89 | export { createUser, loginUser }; 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | public 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # Snowpack dependency directory (https://snowpack.dev/) 47 | web_modules/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Optional stylelint cache 59 | .stylelintcache 60 | 61 | # Microbundle cache 62 | .rpt2_cache/ 63 | .rts2_cache_cjs/ 64 | .rts2_cache_es/ 65 | .rts2_cache_umd/ 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variable files 77 | .env 78 | .env.development.local 79 | .env.test.local 80 | .env.production.local 81 | .env.local 82 | 83 | # parcel-bundler cache (https://parceljs.org/) 84 | .cache 85 | .parcel-cache 86 | 87 | # Next.js build output 88 | .next 89 | out 90 | 91 | # Nuxt.js build / generate output 92 | .nuxt 93 | dist 94 | 95 | # Gatsby files 96 | .cache/ 97 | # Comment in the public line in if your project uses Gatsby and not Next.js 98 | # https://nextjs.org/blog/next-9-1#public-directory-support 99 | # public 100 | 101 | # vuepress build output 102 | .vuepress/dist 103 | 104 | # vuepress v2.x temp and cache directory 105 | .temp 106 | .cache 107 | 108 | # Docusaurus cache and generated files 109 | .docusaurus 110 | 111 | # Serverless directories 112 | .serverless/ 113 | 114 | # FuseBox cache 115 | .fusebox/ 116 | 117 | # DynamoDB Local files 118 | .dynamodb/ 119 | 120 | # TernJS port file 121 | .tern-port 122 | 123 | # Stores VSCode versions used for testing VSCode extensions 124 | .vscode-test 125 | 126 | # yarn v2 127 | .yarn/cache 128 | .yarn/unplugged 129 | .yarn/build-state.yml 130 | .yarn/install-state.gz 131 | .pnp.* 132 | -------------------------------------------------------------------------------- /src/book/bookController.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import fs from "node:fs"; 3 | import { Request, Response, NextFunction } from "express"; 4 | import cloudinary from "../config/cloudinary"; 5 | import createHttpError from "http-errors"; 6 | import bookModel from "./bookModel"; 7 | import { AuthRequest } from "../middlewares/authenticate"; 8 | import userModel from "../user/userModel"; 9 | 10 | const createBook = async (req: Request, res: Response, next: NextFunction) => { 11 | const { title, genre, description } = req.body; 12 | 13 | const files = req.files as { [fieldname: string]: Express.Multer.File[] }; 14 | // 'application/pdf' 15 | const coverImageMimeType = files.coverImage[0].mimetype.split("/").at(-1); 16 | const fileName = files.coverImage[0].filename; 17 | const filePath = path.resolve( 18 | __dirname, 19 | "../../public/data/uploads", 20 | fileName 21 | ); 22 | 23 | try { 24 | const uploadResult = await cloudinary.uploader.upload(filePath, { 25 | filename_override: fileName, 26 | folder: "book-covers", 27 | format: coverImageMimeType, 28 | }); 29 | 30 | const bookFileName = files.file[0].filename; 31 | const bookFilePath = path.resolve( 32 | __dirname, 33 | "../../public/data/uploads", 34 | bookFileName 35 | ); 36 | 37 | const bookFileUploadResult = await cloudinary.uploader.upload( 38 | bookFilePath, 39 | { 40 | resource_type: "raw", 41 | filename_override: bookFileName, 42 | folder: "book-pdfs", 43 | format: "pdf", 44 | } 45 | ); 46 | const _req = req as AuthRequest; 47 | 48 | const newBook = await bookModel.create({ 49 | title, 50 | description, 51 | genre, 52 | author: _req.userId, 53 | coverImage: uploadResult.secure_url, 54 | file: bookFileUploadResult.secure_url, 55 | }); 56 | 57 | // Delete temp.files 58 | // todo: wrap in try catch... 59 | await fs.promises.unlink(filePath); 60 | await fs.promises.unlink(bookFilePath); 61 | 62 | res.status(201).json({ id: newBook._id }); 63 | } catch (err) { 64 | console.log(err); 65 | return next(createHttpError(500, "Error while uploading the files.")); 66 | } 67 | }; 68 | 69 | const updateBook = async (req: Request, res: Response, next: NextFunction) => { 70 | const { title, description, genre } = req.body; 71 | const bookId = req.params.bookId; 72 | 73 | const book = await bookModel.findOne({ _id: bookId }); 74 | 75 | if (!book) { 76 | return next(createHttpError(404, "Book not found")); 77 | } 78 | // Check access 79 | const _req = req as AuthRequest; 80 | if (book.author.toString() !== _req.userId) { 81 | return next(createHttpError(403, "You can not update others book.")); 82 | } 83 | 84 | // check if image field is exists. 85 | 86 | const files = req.files as { [fieldname: string]: Express.Multer.File[] }; 87 | let completeCoverImage = ""; 88 | if (files.coverImage) { 89 | const filename = files.coverImage[0].filename; 90 | const converMimeType = files.coverImage[0].mimetype.split("/").at(-1); 91 | // send files to cloudinary 92 | const filePath = path.resolve( 93 | __dirname, 94 | "../../public/data/uploads/" + filename 95 | ); 96 | completeCoverImage = filename; 97 | const uploadResult = await cloudinary.uploader.upload(filePath, { 98 | filename_override: completeCoverImage, 99 | folder: "book-covers", 100 | format: converMimeType, 101 | }); 102 | 103 | completeCoverImage = uploadResult.secure_url; 104 | await fs.promises.unlink(filePath); 105 | } 106 | 107 | // check if file field is exists. 108 | let completeFileName = ""; 109 | if (files.file) { 110 | const bookFilePath = path.resolve( 111 | __dirname, 112 | "../../public/data/uploads/" + files.file[0].filename 113 | ); 114 | 115 | const bookFileName = files.file[0].filename; 116 | completeFileName = bookFileName; 117 | 118 | const uploadResultPdf = await cloudinary.uploader.upload(bookFilePath, { 119 | resource_type: "raw", 120 | filename_override: completeFileName, 121 | folder: "book-pdfs", 122 | format: "pdf", 123 | }); 124 | 125 | completeFileName = uploadResultPdf.secure_url; 126 | await fs.promises.unlink(bookFilePath); 127 | } 128 | 129 | const updatedBook = await bookModel.findOneAndUpdate( 130 | { 131 | _id: bookId, 132 | }, 133 | { 134 | title: title, 135 | description: description, 136 | genre: genre, 137 | coverImage: completeCoverImage 138 | ? completeCoverImage 139 | : book.coverImage, 140 | file: completeFileName ? completeFileName : book.file, 141 | }, 142 | { new: true } 143 | ); 144 | 145 | res.json(updatedBook); 146 | }; 147 | 148 | const listBooks = async (req: Request, res: Response, next: NextFunction) => { 149 | // const sleep = await new Promise((resolve) => setTimeout(resolve, 5000)); 150 | 151 | try { 152 | // todo: add pagination. 153 | const book = await bookModel.find().populate("author", "name"); 154 | res.json(book); 155 | } catch (err) { 156 | return next(createHttpError(500, "Error while getting a book")); 157 | } 158 | }; 159 | 160 | const getSingleBook = async ( 161 | req: Request, 162 | res: Response, 163 | next: NextFunction 164 | ) => { 165 | const bookId = req.params.bookId; 166 | 167 | try { 168 | const book = await bookModel 169 | .findOne({ _id: bookId }) 170 | // populate author field 171 | .populate("author", "name"); 172 | if (!book) { 173 | return next(createHttpError(404, "Book not found.")); 174 | } 175 | 176 | return res.json(book); 177 | } catch (err) { 178 | return next(createHttpError(500, "Error while getting a book")); 179 | } 180 | }; 181 | 182 | const deleteBook = async (req: Request, res: Response, next: NextFunction) => { 183 | const bookId = req.params.bookId; 184 | 185 | const book = await bookModel.findOne({ _id: bookId }); 186 | if (!book) { 187 | return next(createHttpError(404, "Book not found")); 188 | } 189 | 190 | // Check Access 191 | const _req = req as AuthRequest; 192 | if (book.author.toString() !== _req.userId) { 193 | return next(createHttpError(403, "You can not update others book.")); 194 | } 195 | // book-covers/dkzujeho0txi0yrfqjsm 196 | // https://res.cloudinary.com/degzfrkse/image/upload/v1712590372/book-covers/u4bt9x7sv0r0cg5cuynm.png 197 | 198 | const coverFileSplits = book.coverImage.split("/"); 199 | const coverImagePublicId = 200 | coverFileSplits.at(-2) + 201 | "/" + 202 | coverFileSplits.at(-1)?.split(".").at(-2); 203 | 204 | const bookFileSplits = book.file.split("/"); 205 | const bookFilePublicId = 206 | bookFileSplits.at(-2) + "/" + bookFileSplits.at(-1); 207 | console.log("bookFilePublicId", bookFilePublicId); 208 | // todo: add try error block 209 | await cloudinary.uploader.destroy(coverImagePublicId); 210 | await cloudinary.uploader.destroy(bookFilePublicId, { 211 | resource_type: "raw", 212 | }); 213 | 214 | await bookModel.deleteOne({ _id: bookId }); 215 | 216 | return res.sendStatus(204); 217 | }; 218 | 219 | export { createBook, updateBook, listBooks, getSingleBook, deleteBook }; 220 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 42 | // "resolveJsonModule": true, /* Enable importing .json files. */ 43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | 46 | /* JavaScript Support */ 47 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 50 | 51 | /* Emit */ 52 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 68 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 75 | 76 | /* Interop Constraints */ 77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 83 | 84 | /* Type Checking */ 85 | "strict": true, /* Enable all strict type-checking options. */ 86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 104 | 105 | /* Completeness */ 106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 108 | } 109 | } 110 | --------------------------------------------------------------------------------