commit 81f980148797d7c565dcd1f1a2e591e4359b9369 Author: dev-abumahid Date: Thu Apr 2 21:27:09 2026 +0600 init: init project diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..bdb87c7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +node_modules +npm-debug.log +Dockerfile +.git +.gitignore +README.md +.env +dist diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8a20513 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules +.env +.env.dev +.env.example +.env.prod +package-lock.json +prisma/generated/ \ No newline at end of file diff --git a/.projectrc.json b/.projectrc.json new file mode 100644 index 0000000..0eeaa2d --- /dev/null +++ b/.projectrc.json @@ -0,0 +1,3 @@ +{ + "database": "prisma" +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..254f135 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +# ---------- BUILD STAGE ---------- +FROM node:18-alpine AS builder + +WORKDIR /app + +COPY package*.json ./ +RUN npm install + +COPY . . +RUN npm run build + + +# ---------- PRODUCTION STAGE ---------- +FROM node:18-alpine + +WORKDIR /app + +COPY package*.json ./ +RUN npm install && npm cache clean --force + +COPY --from=builder /app/dist ./dist + +RUN mkdir -p /app/uploads + +EXPOSE 5000 + +CMD ["node", "dist/server.js"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6701a15 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,27 @@ +version: "3.9" + +services: + postgres: + image: postgres:16-alpine + container_name: server_database + restart: always + + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres123 + POSTGRES_DB: server_db + + ports: + - "5432:5432" + + volumes: + - postgres_data:/var/lib/postgresql/data + + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + +volumes: + postgres_data: diff --git a/package.json b/package.json new file mode 100644 index 0000000..64ed3b9 --- /dev/null +++ b/package.json @@ -0,0 +1,47 @@ +{ + "name": "server", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "dev": "tsx watch src/server.ts", + "build": "tsc" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "module", + "dependencies": { + "@prisma/adapter-pg": "^7.5.0", + "@prisma/client": "^7.5.0", + "bcrypt": "^6.0.0", + "cloudinary": "^2.7.0", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "dotenv": "^17.3.1", + "express": "^5.1.0", + "jsonwebtoken": "^9.0.2", + "multer": "^2.0.2", + "nodemailer": "^7.0.9", + "pg": "^8.20.0", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", + "zod": "^4.1.12" + }, + "devDependencies": { + "@types/bcrypt": "^6.0.0", + "@types/cookie-parser": "^1.4.9", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", + "@types/jsonwebtoken": "^9.0.10", + "@types/multer": "^2.0.0", + "@types/node": "^24.10.9", + "@types/nodemailer": "^7.0.2", + "@types/pg": "^8.20.0", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.8", + "prisma": "^7.5.0", + "tsx": "^4.21.0" + } +} diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 0000000..86a210c --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,14 @@ +// This file was generated by Prisma, and assumes you have installed the following: +// npm install --save-dev prisma dotenv +import "dotenv/config"; +import { defineConfig } from "prisma/config"; + +export default defineConfig({ + schema: "prisma/schema", + migrations: { + path: "prisma/migrations", + }, + datasource: { + url: process.env["DATABASE_URL"], + }, +}); diff --git a/prisma/migrations/20260402152356_init/migration.sql b/prisma/migrations/20260402152356_init/migration.sql new file mode 100644 index 0000000..e5703e0 --- /dev/null +++ b/prisma/migrations/20260402152356_init/migration.sql @@ -0,0 +1,37 @@ +-- CreateEnum +CREATE TYPE "ROLE" AS ENUM ('ADMIN', 'USER'); + +-- CreateTable +CREATE TABLE "Account" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "password" TEXT NOT NULL, + "role" "ROLE" NOT NULL DEFAULT 'USER', + "lastOtp" TEXT, + "lastOtpSendingTime" TIMESTAMP(3), + "isDeleted" BOOLEAN NOT NULL DEFAULT false, + "isAccountVerified" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Account_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Profile" ( + "id" TEXT NOT NULL, + "accountId" TEXT NOT NULL, + "fullName" TEXT NOT NULL, + "profilePhoto" TEXT, + + CONSTRAINT "Profile_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Account_email_key" ON "Account"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Profile_accountId_key" ON "Profile"("accountId"); + +-- AddForeignKey +ALTER TABLE "Profile" ADD CONSTRAINT "Profile_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/prisma/schema/account.schema.prisma b/prisma/schema/account.schema.prisma new file mode 100644 index 0000000..1c4fd01 --- /dev/null +++ b/prisma/schema/account.schema.prisma @@ -0,0 +1,19 @@ +enum ROLE { + ADMIN + USER +} + +model Account { + id String @id @default(uuid()) + email String @unique + password String + role ROLE @default(USER) + lastOtp String? + lastOtpSendingTime DateTime? + isDeleted Boolean @default(false) + isAccountVerified Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) + + profile Profile? +} diff --git a/prisma/schema/profile.schema.prisma b/prisma/schema/profile.schema.prisma new file mode 100644 index 0000000..aaf92bb --- /dev/null +++ b/prisma/schema/profile.schema.prisma @@ -0,0 +1,8 @@ +model Profile { + id String @id @default(uuid()) + accountId String @unique + account Account @relation(fields: [accountId], references: [id], onDelete: Cascade) + + fullName String + profilePhoto String? +} diff --git a/prisma/schema/schema.prisma b/prisma/schema/schema.prisma new file mode 100644 index 0000000..2500a9d --- /dev/null +++ b/prisma/schema/schema.prisma @@ -0,0 +1,8 @@ +generator client { + provider = "prisma-client" + output = "../generated/prisma" +} + +datasource db { + provider = "postgresql" +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..9a821dc --- /dev/null +++ b/readme.md @@ -0,0 +1,126 @@ +# ⚡ Express Server CLI + +[![npm version](https://img.shields.io/npm/v/exp-node-server.svg)](https://www.npmjs.com/package/exp-node-server) +[![npm downloads](https://img.shields.io/npm/dw/exp-node-server.svg)](https://www.npmjs.com/package/exp-node-server) +[![npm total downloads](https://img.shields.io/npm/dt/exp-node-server.svg)](https://www.npmjs.com/package/exp-node-server) +[![Made with TypeScript](https://img.shields.io/badge/Made%20with-TypeScript-blue.svg)](https://www.typescriptlang.org/) +[![Node.js](https://img.shields.io/badge/Node.js-18+-green.svg)](https://nodejs.org/) + +--- + +## 🚀 Overview + +A powerful `Express + TypeScript CLI` that instantly generates scalable `backend` modules with `Mongoose `/ `Prisma`, `Zod` `validation`, and `Swagger` documentation. + +This tool helps developers quickly scaffold `clean`, `modular` `Express` APIs with minimal setup. + +## ✨ Features + +- ⚡ Generate complete `Express` + `TypeScript` modules +- 🧩 Built-in `Mongoose` / `Prisma` support +- 📘 Adds Swagger documentation automatically +- 📘 Automatic `Swagger` documentation +- 🔐 `Zod` validation ready +- 🏗️ `Modular` clean architecture +- 🚀 One command project setup +- 🔄 Add modules anytime +- 📦 Zero boilerplate setup + +## 📦 Quick Start + +Run directly using npx (recommended): + +```Bash +npx exp-node-server -c my-api +``` + +This will: + +- Create an Express starter project +- Install dependencies +- Prepare the project for development + +## 🧩 Generate a Module + +Inside your project run: + +```bash +npx express-server-cli -g +``` + +Example: `npx express-server-cli -g order` + +Output: + +```Bash +✔ order.interface.ts created +✔ order.schema.ts created +✔ order.validation.ts created +✔ order.route.ts created +✔ order.controller.ts created +✔ order.service.ts created +✔ order.swagger.ts created + +🔗 Route registered in routes.ts +📘 Swagger docs registered + +``` + +## 📁 Generated Module Structure + +Each module follows a clean structure: + +```Bash +src/app/modules// + ├── .interface.ts + ├── .schema.ts + ├── .validation.ts + ├── .route.ts + ├── .controller.ts + ├── .service.ts + └── .swagger.ts +``` + +## ⚙️ Run the Project + +```bash +npm run dev +``` + +Swagger docs available at: + +```bash +http://localhost:5000/docs +``` + +## 🧠 Tech Stack + +- Express.js — Backend framework +- TypeScript — Strongly typed JavaScript +- Mongoose — MongoDB ODM +- Prisma — PostgreSQL ORM +- Zod — Runtime schema validation +- JWT — Authentication +- Swagger — API documentation + +## 👨‍💻 Author + +### Abumahid + +GitHub +https://github.com/dev-abumahid + +npm +https://www.npmjs.com/~dev_abumahid + +Portfolio +https://abumahid.me + +LinkedIn +https://linkedin.com/in/md-abu-mahid-islam + +## ⭐ Support + +If you find this project helpful, consider giving it a ⭐ on `GitHub`. + +It helps the project grow and reach more developers. diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..0dbfa9f --- /dev/null +++ b/src/app.ts @@ -0,0 +1,42 @@ +import cookieParser from 'cookie-parser'; +import cors from 'cors'; +import express, { Request, Response } from 'express'; +import swaggerJSDoc from 'swagger-jsdoc'; +import swaggerUi from "swagger-ui-express"; +import globalErrorHandler from './app/middlewares/global_error_handler'; +import notFound from './app/middlewares/not_found_api'; +import appRouter from './routes'; +import { swaggerOptions } from './swaggerOptions'; + +// define app +const app = express() +const swaggerSpec = swaggerJSDoc(swaggerOptions); +app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec)); + +// middleware +app.use(cors({ + origin: ["http://localhost:3000"], + methods: ["GET", "POST", "PATCH", "DELETE", "PUT"], + credentials: true +})) +app.use(express.json({ limit: "100mb" })) +app.use(express.raw()) +app.use(cookieParser()) +app.use(express.urlencoded({ extended: true })); +app.use("/api", appRouter) + +// stating point +app.get('/', (req: Request, res: Response) => { + res.status(200).json({ + status: 'success', + message: 'Server is running successful !!', + data: null, + }); +}); + +// global error handler +app.use(globalErrorHandler); +app.use(notFound); + +// export app +export default app; \ No newline at end of file diff --git a/src/app/configs/index.ts b/src/app/configs/index.ts new file mode 100644 index 0000000..54e0554 --- /dev/null +++ b/src/app/configs/index.ts @@ -0,0 +1,26 @@ +import "dotenv/config"; + +export const configs = { + port: process.env.PORT, + env: process.env.NODE_ENV, + db_url: process.env.DATABASE_URL, + jwt: { + access_token: process.env.ACCESS_TOKEN, + refresh_token: process.env.REFRESH_TOKEN, + access_expires: process.env.ACCESS_EXPIRES, + refresh_expires: process.env.REFRESH_EXPIRES, + reset_secret: process.env.RESET_SECRET, + reset_expires: process.env.RESET_EXPIRES, + front_end_url: process.env.FRONT_END_URL, + verified_token: process.env.VERIFIED_TOKEN, + }, + email: { + app_email: process.env.APP_USER_EMAIL, + app_password: process.env.APP_PASSWORD, + }, + cloudinary: { + cloud_name: process.env.CLOUD_NAME, + cloud_api_key: process.env.CLOUD_API_KEY, + cloud_api_secret: process.env.CLOUD_API_SECRET, + }, +}; diff --git a/src/app/errors/zodError.ts b/src/app/errors/zodError.ts new file mode 100644 index 0000000..bc221a3 --- /dev/null +++ b/src/app/errors/zodError.ts @@ -0,0 +1,20 @@ +import { ZodError, ZodIssue } from 'zod' +import { TErrorSources, TGenericErrorResponse } from '../types/error' + +const handleZodError = (err: ZodError): TGenericErrorResponse => { + const errorSources: TErrorSources = err.issues.map((issue: ZodIssue) => { + return { + path: issue?.path[issue.path.length - 1] as string, + message: issue.message + } + }) + + const statusCode = 400 + return { + statusCode, + message: 'Validation Error', + errorSources + } +} + +export default handleZodError \ No newline at end of file diff --git a/src/app/lib/prisma.ts b/src/app/lib/prisma.ts new file mode 100644 index 0000000..ab78aa6 --- /dev/null +++ b/src/app/lib/prisma.ts @@ -0,0 +1,10 @@ +import { PrismaPg } from "@prisma/adapter-pg"; +import "dotenv/config"; +import { PrismaClient } from "../../../prisma/generated/prisma/client"; + +const connectionString = `${process.env.DATABASE_URL}`; + +const adapter = new PrismaPg({ connectionString }); +const prisma = new PrismaClient({ adapter }); + +export { prisma }; diff --git a/src/app/middlewares/auth.ts b/src/app/middlewares/auth.ts new file mode 100644 index 0000000..5723cf3 --- /dev/null +++ b/src/app/middlewares/auth.ts @@ -0,0 +1,30 @@ +import { NextFunction, Request, Response } from "express"; +import { configs } from "../configs"; +import { AppError } from "../utils/app_error"; +import { jwtHelpers, JwtPayloadType } from "../utils/JWT"; + +type Role = "ADMIN" | "USER"; + +const auth = (...roles: Role[]) => { + return async (req: Request, res: Response, next: NextFunction) => { + try { + const token = req.headers.authorization || req.cookies.access_token; + if (!token) { + throw new AppError("You are not authorize!!", 401); + } + const verifiedUser = jwtHelpers.verifyToken( + token, + configs.jwt.access_token as string, + ); + if (!roles.length || !roles.includes(verifiedUser.role)) { + throw new AppError("You are not authorize!!", 401); + } + req.user = verifiedUser as JwtPayloadType; + next(); + } catch (err) { + next(err); + } + }; +}; + +export default auth; diff --git a/src/app/middlewares/global_error_handler.ts b/src/app/middlewares/global_error_handler.ts new file mode 100644 index 0000000..c361f7f --- /dev/null +++ b/src/app/middlewares/global_error_handler.ts @@ -0,0 +1,51 @@ +import { ErrorRequestHandler } from "express"; +import { ZodError } from "zod"; +import { configs } from "../configs"; +import handleZodError from "../errors/zodError"; +import { TErrorSources } from "../types/error"; +import { AppError } from "../utils/app_error"; + +const globalErrorHandler: ErrorRequestHandler = (err, req, res, next) => { + let statusCode = 500; + let message = "Something went wrong!"; + let errorSources: TErrorSources = [ + { + path: "", + message: "Something went wrong", + }, + ]; + + if (err instanceof ZodError) { + const simplifiedError = handleZodError(err); + statusCode = simplifiedError?.statusCode; + message = simplifiedError?.message; + errorSources = simplifiedError?.errorSources; + } else if (err instanceof AppError) { + statusCode = err?.statusCode; + message = err.message; + errorSources = [ + { + path: "", + message: err?.message, + }, + ]; + } else if (err instanceof Error) { + message = err.message; + errorSources = [ + { + path: "", + message: err?.message, + }, + ]; + } + + res.status(statusCode).json({ + success: false, + message, + errorSources, + err, + stack: configs.env === "development" ? err?.stack : null, + }); +}; + +export default globalErrorHandler; diff --git a/src/app/middlewares/not_found_api.ts b/src/app/middlewares/not_found_api.ts new file mode 100644 index 0000000..d52c133 --- /dev/null +++ b/src/app/middlewares/not_found_api.ts @@ -0,0 +1,10 @@ +import { Request, Response, NextFunction } from 'express'; + +const notFound = (req: Request, res: Response, next: NextFunction) => { + res.status(404).json({ + message: 'Sorry Route is not found!! 😴😴😴', + success: false, + error: '', + }); +}; +export default notFound; \ No newline at end of file diff --git a/src/app/middlewares/request_validator.ts b/src/app/middlewares/request_validator.ts new file mode 100644 index 0000000..a860e8e --- /dev/null +++ b/src/app/middlewares/request_validator.ts @@ -0,0 +1,14 @@ +import { NextFunction, Request, Response } from "express"; + +const RequestValidator = (schema: any) => { + return async (req: Request, res: Response, next: NextFunction) => { + try { + req.body = await schema.parseAsync(req.body); + next(); + } catch (err) { + next(err); + } + }; +}; + +export default RequestValidator; diff --git a/src/app/middlewares/uploader.ts b/src/app/middlewares/uploader.ts new file mode 100644 index 0000000..2c63ee5 --- /dev/null +++ b/src/app/middlewares/uploader.ts @@ -0,0 +1,16 @@ +import multer from "multer"; +import path from "path"; + +const storage = multer.diskStorage({ + destination: function (req, file, cb) { + cb(null, path.join(process.cwd(), "uploads")) + }, + filename: function (req, file, cb) { + + cb(null, file.originalname) + } +}) + +const uploader = multer({ storage: storage }) + +export default uploader; \ No newline at end of file diff --git a/src/app/modules/account/account.controller.ts b/src/app/modules/account/account.controller.ts new file mode 100644 index 0000000..0f753fa --- /dev/null +++ b/src/app/modules/account/account.controller.ts @@ -0,0 +1,116 @@ +import { configs } from "../../configs"; +import catchAsync from "../../utils/catch_async"; +import manageResponse from "../../utils/manage_response"; +import { account_services } from "./account.service"; + +const create_account = catchAsync(async (req, res) => { + const result = await account_services.create_account_into_db(req); + manageResponse(res, { + statusCode: 200, + success: true, + message: "Account created successfully", + data: result, + }); +}); + +const verify_account_using_otp = catchAsync(async (req, res) => { + const result = await account_services.verify_account_using_otp_into_db(req); + manageResponse(res, { + statusCode: 200, + success: true, + message: "Otp verification successfull", + data: result, + }); +}); + +const verify_account_using_link = catchAsync(async (req, res) => { + const result = await account_services.verify_account_using_link_into_db(req); + manageResponse(res, { + statusCode: 200, + success: true, + message: "Account verification successfull", + data: result, + }); +}); + +const login_user = catchAsync(async (req, res) => { + const result = await account_services.login_user_into_db(req); + + // set access token into cookie + res.cookie("access_token", result, { + secure: configs.env === "production", + httpOnly: true, + }); + + manageResponse(res, { + statusCode: 200, + success: true, + message: "User logged in successfully", + data: { + accessToken: result, + }, + }); +}); +const get_user_account = catchAsync(async (req, res) => { + const result = await account_services.get_user_account_from_db(req); + + manageResponse(res, { + statusCode: 200, + success: true, + message: "Account fetched successfully", + data: result, + }); +}); + +const change_password = catchAsync(async (req, res) => { + const result = await account_services.change_password_into_db(req); + manageResponse(res, { + statusCode: 200, + success: true, + message: "Password Change successfully", + data: result, + }); +}); + +const resend_otp_and_verification_link = catchAsync(async (req, res) => { + const result = + await account_services.resend_otp_and_verification_link_from_db(req); + manageResponse(res, { + statusCode: 200, + success: true, + message: "OTP reset successfully", + data: result, + }); +}); + +const forget_password_genereate_reset_token = catchAsync(async (req, res) => { + const result = + await account_services.forget_password_genereate_reset_token_from_db(req); + manageResponse(res, { + statusCode: 200, + success: true, + message: "Password reset successfully", + data: result, + }); +}); + +const reset_password_using_token = catchAsync(async (req, res) => { + const result = await account_services.reset_password_using_token_into_db(req); + manageResponse(res, { + statusCode: 200, + success: true, + message: "Password reset successfully", + data: result, + }); +}); +export const account_controller = { + create_account, + login_user, + get_user_account, + change_password, + verify_account_using_otp, + resend_otp_and_verification_link, + verify_account_using_link, + forget_password_genereate_reset_token, + reset_password_using_token +}; diff --git a/src/app/modules/account/account.route.ts b/src/app/modules/account/account.route.ts new file mode 100644 index 0000000..3f580d2 --- /dev/null +++ b/src/app/modules/account/account.route.ts @@ -0,0 +1,55 @@ +import { Router } from "express"; +import auth from "../../middlewares/auth"; +import RequestValidator from "../../middlewares/request_validator"; +import { account_controller } from "./account.controller"; +import { account_validation } from "./account.validation"; + +const accountRouter = Router(); + +accountRouter.post( + "/sign-up", + RequestValidator(account_validation.sign_up), + account_controller.create_account, +); +accountRouter.post( + "/sign-in", + RequestValidator(account_validation.sing_in), + account_controller.login_user, +); +accountRouter.put( + "/verify-otp", + RequestValidator(account_validation.verify_otp), + account_controller.verify_account_using_otp, +); +accountRouter.put( + "/verify-link", + RequestValidator(account_validation.verify_link), + account_controller.verify_account_using_link, +); +accountRouter.get( + "/me", + auth("USER", "ADMIN"), + account_controller.get_user_account, +); +accountRouter.put( + "/change-password", + auth("USER", "ADMIN"), + RequestValidator(account_validation.change_password), + account_controller.change_password, +); +accountRouter.put( + "/resend-otp", + RequestValidator(account_validation.resend_otp), + account_controller.resend_otp_and_verification_link, +); +accountRouter.put( + "/forget-password", + RequestValidator(account_validation.resend_otp), + account_controller.forget_password_genereate_reset_token, +); +accountRouter.put( + "/reset-password", + RequestValidator(account_validation.reset_pass), + account_controller.reset_password_using_token, +); +export default accountRouter; diff --git a/src/app/modules/account/account.service.ts b/src/app/modules/account/account.service.ts new file mode 100644 index 0000000..043a618 --- /dev/null +++ b/src/app/modules/account/account.service.ts @@ -0,0 +1,402 @@ +import bcrypt from "bcrypt"; +import { Request } from "express"; +import { configs } from "../../configs"; +import { prisma } from "../../lib/prisma"; +import { AppError } from "../../utils/app_error"; +import { jwtHelpers } from "../../utils/JWT"; +import { otpGenerator } from "../../utils/otpGenerator"; +import sendMail from "../../utils/mail_sender"; + +const create_account_into_db = async (req: Request) => { + const payload = req?.body; + + // hash password + const hashPassword = bcrypt.hashSync(payload.password, 10); + + // create account and profile + const result = await prisma.$transaction(async (tx) => { + const account = await tx.account.create({ + data: { + email: payload.email, + password: hashPassword, + }, + }); + + const profile = await tx.profile.create({ + data: { + fullName: payload.fullName, + accountId: account.id, + }, + }); + + return { + account, + profile, + }; + }); + + // sending otp and verification link + const newOtp = otpGenerator(); + const verificationToken = jwtHelpers.generateToken( + { + email: payload.email, + accountId: result.account.id, + }, + configs.jwt.verified_token as string, + "5m", + ); + const verificationLink = `${configs.jwt.front_end_url}/verify/token?=${verificationToken}`; + // save otp into db + await prisma.account.update({ + where: { + email: payload.email, + }, + data: { + lastOtp: newOtp, + lastOtpSendingTime: new Date(), + }, + }); + + await sendMail({ + to: payload.email as string, + subject: "welcome to - Please verify your account", + htmlBody: ` +

OTP ${newOtp}

+ Otp will be expire in 5 minutes + +

+ +

Or you can use Verification link

+

${verificationLink}

+ `, + textBody: "You can use otp or direct link", + name: payload.fullName, + }); + return result; +}; + +const verify_account_using_otp_into_db = async (req: Request) => { + const payload: { email: string; otp: string } = req?.body; + // check account + const account = await prisma.account.findUnique({ + where: { + email: payload.email, + }, + }); + // check if account exists + if (!account) { + throw new AppError("Account not found", 404); + } + // match with last otp + const isOtpMatch = payload.otp === account.lastOtp; + if (!isOtpMatch) { + throw new AppError("Invalid OTP, Please try again!!", 401); + } + // check otp timing + const OTP_EXPIRY_TIME = 5 * 60 * 1000; // 5 minutes in ms + const isOtpExpired = account.lastOtpSendingTime + ? new Date().getTime() - new Date(account.lastOtpSendingTime).getTime() > + OTP_EXPIRY_TIME + : true; + + if (isOtpExpired) { + throw new AppError("OTP Expired, Please try again!!", 401); + } + // change account status + await prisma.account.update({ + where: { + id: account.id, + }, + data: { + isAccountVerified: true, + }, + }); + // infuter user welcome email + return ""; +}; + +const verify_account_using_link_into_db = async (req: Request) => { + const token = req?.body?.token as string; + let decoadeToken: any; + try { + decoadeToken = jwtHelpers.verifyToken( + token, + configs.jwt.verified_token as string, + ); + } catch (error: any) { + if (error?.message == "invalid signature") { + throw new AppError("Invalid Token", 403); + } else if (error?.message == "jwt expired") { + throw new AppError("Token expired, please reset again", 403); + } + } + // check account + const account = await prisma.account.findUnique({ + where: { + email: decoadeToken.email, + }, + }); + // check if account exists + if (!account) { + throw new AppError("Account not found", 404); + } + + // change account status + await prisma.account.update({ + where: { + id: account.id, + }, + data: { + isAccountVerified: true, + }, + }); + // infuter user welcome email + return ""; +}; + +const login_user_into_db = async (req: Request) => { + const payload = req?.body; + const account = await prisma.account.findUnique({ + where: { + email: payload.email, + }, + }); + + // check if account exists + if (!account) { + throw new AppError("Account not found", 404); + } + + // checking password + const isPasswordMatch = bcrypt.compareSync( + payload.password, + account.password, + ); + + if (!isPasswordMatch) { + throw new AppError("Invalid password", 401); + } + + // check if account is deleted + if (account.isDeleted) { + throw new AppError("Account is deleted", 401); + } + // check if account is verified + if (!account.isAccountVerified) { + throw new AppError("Account is not verified", 401); + } + + // generate access + const accessToken = jwtHelpers.generateToken( + { + email: account.email, + role: account.role, + accountId: account.id, + }, + configs.jwt.access_token as string, + configs.jwt.access_expires as string, + ); + return accessToken; +}; + +const get_user_account_from_db = async (req: Request) => { + const user = req?.user; + + const result = await prisma.account.findUnique({ + where: { + id: user?.accountId, + }, + select: { + id: true, + email: true, + role: true, + isAccountVerified: true, + isDeleted: true, + profile: true, + }, + }); + return result; +}; + +const change_password_into_db = async (req: Request) => { + const user = req?.user; + // payload + const payload: { oldPassword: string; newPassword: string } = req?.body; + // check old and new password is not same + const isSamePassword = payload.oldPassword === payload.newPassword; + if (isSamePassword) { + throw new AppError( + "Old and new password are same, Please provide deffirent password", + 404, + ); + } + // check user validity + const isUserExist = await prisma.account.findFirst({ + where: { + email: user?.email, + }, + }); + // if account not exists + if (!isUserExist) { + throw new AppError("Account not found!!", 404); + } + // check old password + const isPasswordMatch = bcrypt.compareSync( + payload.oldPassword, + isUserExist.password, + ); + if (!isPasswordMatch) { + throw new AppError("Incorrect password", 401); + } + // change password logic + const newHashPassword = bcrypt.hashSync(payload.newPassword, 10); + await prisma.account.update({ + where: { + id: isUserExist.id, + }, + data: { + password: newHashPassword, + }, + }); + + // in future email notification for more sucurity + return ""; +}; + +const resend_otp_and_verification_link_from_db = async (req: Request) => { + const email = req?.body?.email as string; + const account = await prisma.account.findUnique({ + where: { + email, + }, + }); + // check if account exists + if (!account) { + throw new AppError("Account not found", 404); + } + // if already verified + if (account.isAccountVerified) { + throw new AppError("Account already verified", 403); + } + // make new otp and verification link + const newOtp = otpGenerator(); + const verificationToken = jwtHelpers.generateToken( + { + email, + accountId: account.id, + }, + configs.jwt.verified_token as string, + "5m", + ); + const verificationLink = `${configs.jwt.front_end_url}/verify/token?=${verificationToken}`; + // save otp into db + await prisma.account.update({ + where: { + email, + }, + data: { + lastOtp: newOtp, + lastOtpSendingTime: new Date(), + }, + }); + + await sendMail({ + to: email as string, + subject: "New Verification otp and link", + htmlBody: ` +

OTP ${newOtp}

+ Otp will be expire in 5 minutes + +

+ +

Or you can use Verification link

+

${verificationLink}

+ `, + textBody: "You can use otp or direct link", + }); +}; + +const forget_password_genereate_reset_token_from_db = async (req: Request) => { + const email = req?.body?.email as string; + const account = await prisma.account.findUnique({ + where: { + email, + }, + }); + // check if account exists + if (!account) { + throw new AppError("Account not found", 404); + } + // generate forget token + const verificationToken = jwtHelpers.generateToken( + { + email: email, + accountId: account.id, + }, + configs.jwt.verified_token as string, + "5m", + ); + const verificationLink = `${configs.jwt.front_end_url}/verify/token?=${verificationToken}`; + + await sendMail({ + to: email as string, + subject: "Forget Password- Use this link for new password ", + htmlBody: ` +

Your Reset Link:

+

${verificationLink}

+ `, + textBody: "", + }); +}; + +const reset_password_using_token_into_db = async (req: Request) => { + const token = req?.body?.token as string; + const newPass = req?.body?.newPass as string; + let decoadeToken: any; + try { + decoadeToken = jwtHelpers.verifyToken( + token, + configs.jwt.verified_token as string, + ); + } catch (error: any) { + if (error?.message == "invalid signature") { + throw new AppError("Invalid Token", 403); + } else if (error?.message == "jwt expired") { + throw new AppError("Link expired, please reset again", 403); + } + } + // check account + const account = await prisma.account.findUnique({ + where: { + email: decoadeToken.email, + }, + }); + // check if account exists + if (!account) { + throw new AppError("Account not found", 404); + } + + // change account password + const newHash = bcrypt.hashSync(newPass, 10); + await prisma.account.update({ + where: { + id: account.id, + }, + data: { + password: newHash, + }, + }); + // infuter user alart for changing password + return ""; +}; +export const account_services = { + create_account_into_db, + login_user_into_db, + get_user_account_from_db, + change_password_into_db, + verify_account_using_otp_into_db, + resend_otp_and_verification_link_from_db, + verify_account_using_link_into_db, + forget_password_genereate_reset_token_from_db, + reset_password_using_token_into_db +}; diff --git a/src/app/modules/account/account.swagger.ts b/src/app/modules/account/account.swagger.ts new file mode 100644 index 0000000..e47ded9 --- /dev/null +++ b/src/app/modules/account/account.swagger.ts @@ -0,0 +1,187 @@ +export const accountSwaggerDocs = { + "/api/auth/sign-up": { + post: { + tags: ["account"], + summary: "Create new account", + description: "", + requestBody: { + required: true, + content: { + "application/json": { + example: JSON.stringify({ + email: "user@gmail.com", + password: "password", + fullName: "User", + }), + }, + }, + }, + responses: { + 201: { description: "account created successfully" }, + 500: { description: "Validation error or internal server error" }, + }, + }, + }, + "/api/auth/sign-in": { + post: { + tags: ["account"], + summary: "Sign In your account", + description: "", + requestBody: { + required: true, + content: { + "application/json": { + example: JSON.stringify({ + email: "user@gmail.com", + password: "password", + }), + }, + }, + }, + responses: { + 201: { description: "User signed in successfully" }, + 500: { description: "Validation error or internal server error" }, + }, + }, + }, + "/api/auth/verify-otp": { + put: { + tags: ["account"], + summary: "Verify OTP", + description: "", + requestBody: { + required: true, + content: { + "application/json": { + example: JSON.stringify({ + email: "user@gmail.com", + otp: "654321", + }), + }, + }, + }, + responses: { + 201: { description: "OTP verification successfully" }, + 500: { description: "Validation error or internal server error" }, + }, + }, + }, + "/api/auth/verify-link": { + put: { + tags: ["account"], + summary: "Verify Link", + description: "", + requestBody: { + required: true, + content: { + "application/json": { + example: JSON.stringify({ + token: "dsakfjasdkj", + }), + }, + }, + }, + responses: { + 201: { description: "Token verification successfully" }, + 500: { description: "Validation error or internal server error" }, + }, + }, + }, + "/api/auth/me": { + get: { + tags: ["account"], + summary: "Get me account", + description: "", + responses: { + 200: { description: "account fetched successfully" }, + 401: { description: "unauthorized" }, + }, + }, + }, + "/api/auth/change-password": { + put: { + tags: ["account"], + summary: "Change Password", + description: "", + requestBody: { + required: true, + content: { + "application/json": { + example: JSON.stringify({ + oldPassword: "123456", + newPassword: "654321", + }), + }, + }, + }, + responses: { + 201: { description: "Passwrod change successfully" }, + 500: { description: "Validation error or internal server error" }, + }, + }, + }, + "/api/auth/resend-otp": { + put: { + tags: ["account"], + summary: "Resend OTP", + description: "", + requestBody: { + required: true, + content: { + "application/json": { + example: JSON.stringify({ + email: "user@gmail.com", + }), + }, + }, + }, + responses: { + 201: { description: "OTP resend successfully" }, + 500: { description: "Validation error or internal server error" }, + }, + }, + }, + "/api/auth/forget-password": { + put: { + tags: ["account"], + summary: "Forget Password", + description: "", + requestBody: { + required: true, + content: { + "application/json": { + example: JSON.stringify({ + email: "user@gmail.com", + }), + }, + }, + }, + responses: { + 201: { description: "Forget password successfully" }, + 500: { description: "Validation error or internal server error" }, + }, + }, + }, + "/api/auth/reset-password": { + put: { + tags: ["account"], + summary: "Reset Password", + description: "", + requestBody: { + required: true, + content: { + "application/json": { + example: JSON.stringify({ + token: "dkfjadskfds", + newPass: "newpass", + }), + }, + }, + }, + responses: { + 201: { description: "Password reset successfully" }, + 500: { description: "Validation error or internal server error" }, + }, + }, + }, +}; diff --git a/src/app/modules/account/account.validation.ts b/src/app/modules/account/account.validation.ts new file mode 100644 index 0000000..c4e38f6 --- /dev/null +++ b/src/app/modules/account/account.validation.ts @@ -0,0 +1,41 @@ +import z from "zod"; + +const sign_up = z.object({ + email: z.string("Email is required."), + password: z.string("Password is required."), + fullName: z.string("Full name is required."), +}); + +const sing_in = z.object({ + email: z.string("Email is required."), + password: z.string("Password is required."), +}); + +const change_password = z.object({ + oldPassword: z.string("Old Password is requied"), + newPassword: z.string("New Password is required"), +}); + +const verify_otp = z.object({ + email: z.string("Email is requied"), + otp: z.string("OTP is required"), +}); +const verify_link = z.object({ + token: z.string("Token is required "), +}); +const resend_otp = z.object({ + email: z.string("Email is requied"), +}); +const reset_pass = z.object({ + token: z.string("Token is required"), + newPass: z.string("Password is required"), +}); +export const account_validation = { + sign_up, + sing_in, + change_password, + verify_otp, + resend_otp, + verify_link, + reset_pass +}; diff --git a/src/app/modules/profile/profile.controller.ts b/src/app/modules/profile/profile.controller.ts new file mode 100644 index 0000000..65a0b55 --- /dev/null +++ b/src/app/modules/profile/profile.controller.ts @@ -0,0 +1,17 @@ +import catchAsync from "../../utils/catch_async"; +import manageResponse from "../../utils/manage_response"; +import { profile_service } from "./profile.service"; + +const update_profile = catchAsync(async (req, res) => { + const result = await profile_service.update_profile_into_db(req); + manageResponse(res, { + success: true, + statusCode: 200, + message: "profile updated successfully.", + data: result, + }); +}); + +export const profile_controller = { + update_profile, +}; diff --git a/src/app/modules/profile/profile.route.ts b/src/app/modules/profile/profile.route.ts new file mode 100644 index 0000000..7095e3b --- /dev/null +++ b/src/app/modules/profile/profile.route.ts @@ -0,0 +1,22 @@ +import { Router } from "express"; +import RequestValidator from "../../middlewares/request_validator"; +import { profile_controller } from "./profile.controller"; +import { profile_validations } from "./profile.validation"; +import auth from "../../middlewares/auth"; +import uploader from "../../middlewares/uploader"; + +const router = Router(); + +router.patch( + "/", + auth("USER"), + uploader.single("file"), + (req, res, next) => { + req.body = JSON.parse(req?.body?.data); + next(); + }, + RequestValidator(profile_validations.update_profile), + profile_controller.update_profile, +); + +export default router; diff --git a/src/app/modules/profile/profile.service.ts b/src/app/modules/profile/profile.service.ts new file mode 100644 index 0000000..06d8a84 --- /dev/null +++ b/src/app/modules/profile/profile.service.ts @@ -0,0 +1,26 @@ +import { Request } from "express"; +import uploadCloud from "../../utils/cloudinary"; +import { prisma } from "../../lib/prisma"; +import { JwtPayloadType } from "../../utils/JWT"; + +const update_profile_into_db = async (req: Request) => { + const user = req?.user as JwtPayloadType; + const payload = req?.body; + const file = req?.file; + // check file and upload to cloud + if (file) { + const cloudRes = await uploadCloud(file); + payload.profilePhoto = cloudRes?.secure_url; + } + const result = await prisma.profile.update({ + where: { + accountId: user.accountId as string, + }, + data: payload, + }); + return result; +}; + +export const profile_service = { + update_profile_into_db, +}; diff --git a/src/app/modules/profile/profile.swagger.ts b/src/app/modules/profile/profile.swagger.ts new file mode 100644 index 0000000..5a93aee --- /dev/null +++ b/src/app/modules/profile/profile.swagger.ts @@ -0,0 +1,34 @@ +export const profileSwaggerDocs = { + "/api/profile": { + patch: { + tags: ["profile"], + summary: "Update profile", + requestBody: { + required: true, + content: { + "multipart/form-data": { + schema: { + type: "object", + properties: { + data: { + type: "object", + properties: { + fullName: { type: "string" }, + }, + }, + file: { + type: "string", + format: "binary", + }, + }, + }, + }, + }, + }, + responses: { + 200: { description: "profile updated successfully" }, + 500: { description: "Validation error or internal server error" }, + }, + }, + }, +}; diff --git a/src/app/modules/profile/profile.validation.ts b/src/app/modules/profile/profile.validation.ts new file mode 100644 index 0000000..b7ffb04 --- /dev/null +++ b/src/app/modules/profile/profile.validation.ts @@ -0,0 +1,8 @@ +import { z } from "zod"; +const update_profile = z.object({ + fullName: z.string().optional(), +}); + +export const profile_validations = { + update_profile, +}; diff --git a/src/app/types/error.d.ts b/src/app/types/error.d.ts new file mode 100644 index 0000000..97c0390 --- /dev/null +++ b/src/app/types/error.d.ts @@ -0,0 +1,10 @@ +export type TErrorSources = { + path: string | number + message: string +}[] + +export type TGenericErrorResponse = { + statusCode: number + message: string + errorSources: TErrorSources +} \ No newline at end of file diff --git a/src/app/types/index.d.ts b/src/app/types/index.d.ts new file mode 100644 index 0000000..dd609d4 --- /dev/null +++ b/src/app/types/index.d.ts @@ -0,0 +1,12 @@ +import { TJwtUser } from "../modules/auth/auth.interface"; +import { JwtPayloadType } from "../utils/JWT"; + + + +declare global { + namespace Express { + interface Request { + user?: JwtPayloadType; + } + } +} diff --git a/src/app/utils/JWT.ts b/src/app/utils/JWT.ts new file mode 100644 index 0000000..1fbbdbb --- /dev/null +++ b/src/app/utils/JWT.ts @@ -0,0 +1,27 @@ +import jwt, { JwtPayload, Secret, SignOptions } from "jsonwebtoken"; + +const generateToken = (payload: object, secret: Secret, expiresIn: string) => { + const token = jwt.sign(payload, secret, { + algorithm: "HS256", + expiresIn: expiresIn, + } as SignOptions); + + return token; +}; + +const verifyToken = (token: string, secret: Secret): JwtPayload => { + return jwt.verify(token, secret) as JwtPayload; +}; + +export const jwtHelpers = { + generateToken, + verifyToken, +}; +export type JwtPayloadType = JwtPayload & { + email: string; + role: string; + accountId: string; + iat: number; + exp: number; +}; +export type JwtTokenType = string | JwtPayloadType | null; diff --git a/src/app/utils/app_error.ts b/src/app/utils/app_error.ts new file mode 100644 index 0000000..523b5c0 --- /dev/null +++ b/src/app/utils/app_error.ts @@ -0,0 +1,12 @@ +export class AppError extends Error { + public statusCode: number; + constructor(message: string, statusCode: number, stack = '') { + super(message); + this.statusCode = statusCode; + if (stack) { + this.stack = stack; + } else { + Error.captureStackTrace(this, this.constructor); + } + } +} \ No newline at end of file diff --git a/src/app/utils/catch_async.ts b/src/app/utils/catch_async.ts new file mode 100644 index 0000000..c0170a5 --- /dev/null +++ b/src/app/utils/catch_async.ts @@ -0,0 +1,13 @@ +import { RequestHandler, Request, Response, NextFunction } from 'express'; + +const catchAsync = (fn: RequestHandler): RequestHandler => { + return async (req: Request, res: Response, next: NextFunction) => { + try { + await fn(req, res, next); + } catch (error) { + next(error); + } + }; +}; + +export default catchAsync; \ No newline at end of file diff --git a/src/app/utils/cloudinary.ts b/src/app/utils/cloudinary.ts new file mode 100644 index 0000000..ae77b6b --- /dev/null +++ b/src/app/utils/cloudinary.ts @@ -0,0 +1,66 @@ +import { v2 as cloudinary } from 'cloudinary'; +import fs from 'fs'; +import { configs } from '../configs'; + +type ICloudinaryResponse = { + asset_id: string; + public_id: string; + version: number; + version_id: string; + signature: string; + width: number; + height: number; + format: string; + resource_type: string; + created_at: string; + tags: string[]; + bytes: number; + type: string; + etag: string; + placeholder: boolean; + url: string; + secure_url: string; + folder: string; + overwritten: boolean; + original_filename: string; + original_extension: string; + api_key: string; +}; + +type IFile = { + fieldname: string; + originalname: string; + encoding: string; + mimetype: string; + destination: string; + filename: string; + path: string; + size: number; +}; + +// Configuration +cloudinary.config({ + cloud_name: configs.cloudinary.cloud_name!, + api_key: configs.cloudinary.cloud_api_key!, + api_secret: configs.cloudinary.cloud_api_secret!, +}); + +const uploadCloud = async ( + file: IFile +): Promise => { + return new Promise((resolve, reject) => { + cloudinary.uploader.upload( + file.path, + (error: Error, result: ICloudinaryResponse) => { + fs.unlinkSync(file.path); + if (error) { + reject(error); + } else { + resolve(result); + } + } + ); + }); +}; + +export default uploadCloud; \ No newline at end of file diff --git a/src/app/utils/mail_sender.ts b/src/app/utils/mail_sender.ts new file mode 100644 index 0000000..4e25247 --- /dev/null +++ b/src/app/utils/mail_sender.ts @@ -0,0 +1,102 @@ +import nodemailer from 'nodemailer'; +import { configs } from '../configs'; +type TMailContent = { + to: string, + subject: string, + textBody: string, + htmlBody: string, + name?: string +} + +const transporter = nodemailer.createTransport({ + host: "smtp.gmail.com", + port: 465, + secure: true, // true for 465, false for other ports + auth: { + user: configs.email.app_email!, + pass: configs.email.app_password!, + }, +}); + +// ✅ Email Sender Function +const sendMail = async (payload: TMailContent) => { + const info = await transporter.sendMail({ + from: 'info@digitalcreditai.com', + to: payload.to, + subject: payload.subject, + text: payload.textBody, + html: ` + + + + + + Welcome Email + + + + + + +
+ +
+

Hi ${payload?.name || ""},

+ + ${payload?.htmlBody} + +
+ + + +

The Support Team

+

Company Name

+
+
+

+ This is an automated message — please do not reply to this email. +
+ If you need assistance, feel free to contact our support team. +

+ Thank you for choosing us! +

+ +
+
+ © {{year}} Your Company. All rights reserved. +
+ +
+ + + + + `, + }); + return info +}; + +export default sendMail; diff --git a/src/app/utils/manage_response.ts b/src/app/utils/manage_response.ts new file mode 100644 index 0000000..24aff0d --- /dev/null +++ b/src/app/utils/manage_response.ts @@ -0,0 +1,24 @@ +import { Response } from "express" +interface IResponse { + success: boolean, + statusCode: number, + message: string, + data?: T, + meta?: { + page?: number, + limit?: number, + skip?: number, + total?: number + } +} + +const manageResponse = (res: Response, payload: IResponse) => { + res.status(payload.statusCode).json({ + success: payload.success, + message: payload.message, + data: payload.data || undefined || null, + meta: payload.meta || undefined || null + }) +} + +export default manageResponse \ No newline at end of file diff --git a/src/app/utils/otpGenerator.ts b/src/app/utils/otpGenerator.ts new file mode 100644 index 0000000..8672640 --- /dev/null +++ b/src/app/utils/otpGenerator.ts @@ -0,0 +1,29 @@ +export const otpGenerator = (): string => { + const upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + const lower = "abcdefghijklmnopqrstuvwxyz"; + const numbers = "0123456789"; + const symbols = "@#$%&*!?"; + + const allChars = upper + lower + numbers + symbols; + + let otp = ""; + + // Ensure at least one character from each set + otp += upper[Math.floor(Math.random() * upper.length)]; + otp += lower[Math.floor(Math.random() * lower.length)]; + otp += numbers[Math.floor(Math.random() * numbers.length)]; + otp += symbols[Math.floor(Math.random() * symbols.length)]; + + // Fill the rest randomly + for (let i = otp.length; i < 6; i++) { + otp += allChars[Math.floor(Math.random() * allChars.length)]; + } + + // Shuffle to remove predictable order + otp = otp + .split("") + .sort(() => Math.random() - 0.5) + .join(""); + + return otp; +}; diff --git a/src/routes.ts b/src/routes.ts new file mode 100644 index 0000000..6327d7a --- /dev/null +++ b/src/routes.ts @@ -0,0 +1,11 @@ +import { Router } from "express"; +import accountRouter from "./app/modules/account/account.route"; +import profileRoute from "./app/modules/profile/profile.route"; + +const appRouter = Router(); + +const moduleRoutes = [ + { path: "/profile", route: profileRoute },{ path: "/auth", route: accountRouter }]; + +moduleRoutes.forEach((route) => appRouter.use(route.path, route.route)); +export default appRouter; diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..875d07d --- /dev/null +++ b/src/server.ts @@ -0,0 +1,19 @@ +import app from "./app"; +import { configs } from "./app/configs/index"; +import { prisma } from "./app/lib/prisma"; + +async function main() { + try { + app.listen(configs.port, async () => { + await prisma.$connect(); + console.log(`Server is running on port ${configs.port}`); + }); + } catch (error) { + console.error("Error starting server:", error); + } +} + +main().catch((error) => { + console.error("Error in main function:", error); + process.exit(1); +}); diff --git a/src/swaggerOptions.ts b/src/swaggerOptions.ts new file mode 100644 index 0000000..c5f49f4 --- /dev/null +++ b/src/swaggerOptions.ts @@ -0,0 +1,48 @@ +import { fileURLToPath } from "node:url"; +import path from "path"; +import { configs } from "./app/configs"; +import { accountSwaggerDocs } from "./app/modules/account/account.swagger"; +import { profileSwaggerDocs } from "./app/modules/profile/profile.swagger"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export const swaggerOptions = { + definition: { + openapi: "3.0.0", + info: { + title: "API Doc - Build with exp-node-server", + version: "1.0.0", + description: "Express + Prisma API with auto-generated Swagger docs", + }, + paths: { + ...accountSwaggerDocs, + ...profileSwaggerDocs, + }, + servers: + configs.env === "production" + ? [{ url: "https://live-url.com" }, { url: "http://localhost:5000" }] + : [{ url: "http://localhost:5000" }, { url: "https://live-url.com" }], + components: { + securitySchemes: { + AuthorizationToken: { + type: "apiKey", + in: "header", + name: "Authorization", + description: "Put your accessToken here ", + }, + }, + }, + security: [ + { + AuthorizationToken: [], + }, + ], + }, + apis: [ + path.join( + __dirname, + configs.env === "production" ? "./**/*.js" : "./**/*.ts", + ), + ], +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f40b4eb --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "ESNext", + "moduleResolution": "bundler", + "rootDir": "./", + "outDir": "./dist", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*", + "prisma/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "build", + "generated/prisma" + ] +} \ No newline at end of file