init: init project

This commit is contained in:
2026-04-02 21:27:09 +06:00
commit 81f9801487
45 changed files with 1857 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
node_modules
npm-debug.log
Dockerfile
.git
.gitignore
README.md
.env
dist
+7
View File
@@ -0,0 +1,7 @@
node_modules
.env
.env.dev
.env.example
.env.prod
package-lock.json
prisma/generated/
+3
View File
@@ -0,0 +1,3 @@
{
"database": "prisma"
}
+27
View File
@@ -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"]
+27
View File
@@ -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:
+47
View File
@@ -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"
}
}
+14
View File
@@ -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"],
},
});
@@ -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;
+3
View File
@@ -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"
+19
View File
@@ -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?
}
+8
View File
@@ -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?
}
+8
View File
@@ -0,0 +1,8 @@
generator client {
provider = "prisma-client"
output = "../generated/prisma"
}
datasource db {
provider = "postgresql"
}
+126
View File
@@ -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 <module-name>
```
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/<module-name>/
├── <module>.interface.ts
├── <module>.schema.ts
├── <module>.validation.ts
├── <module>.route.ts
├── <module>.controller.ts
├── <module>.service.ts
└── <module>.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.
+42
View File
@@ -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;
+26
View File
@@ -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,
},
};
+20
View File
@@ -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
+10
View File
@@ -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 };
+30
View File
@@ -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;
@@ -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;
+10
View File
@@ -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;
+14
View File
@@ -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;
+16
View File
@@ -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;
@@ -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
};
+55
View File
@@ -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;
+402
View File
@@ -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: `
<p><strong>OTP</strong> ${newOtp}</p>
<small>Otp will be expire in 5 minutes</small>
<br/> <br/>
<p>Or you can use Verification link </p>
<p>${verificationLink}</p>
`,
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: `
<p><strong>OTP</strong> ${newOtp}</p>
<small>Otp will be expire in 5 minutes</small>
<br/> <br/>
<p>Or you can use Verification link </p>
<p>${verificationLink}</p>
`,
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: `
<p>Your Reset Link: </p>
<p>${verificationLink}</p>
`,
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
};
+187
View File
@@ -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" },
},
},
},
};
@@ -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
};
@@ -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,
};
+22
View File
@@ -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;
@@ -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,
};
@@ -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" },
},
},
},
};
@@ -0,0 +1,8 @@
import { z } from "zod";
const update_profile = z.object({
fullName: z.string().optional(),
});
export const profile_validations = {
update_profile,
};
+10
View File
@@ -0,0 +1,10 @@
export type TErrorSources = {
path: string | number
message: string
}[]
export type TGenericErrorResponse = {
statusCode: number
message: string
errorSources: TErrorSources
}
+12
View File
@@ -0,0 +1,12 @@
import { TJwtUser } from "../modules/auth/auth.interface";
import { JwtPayloadType } from "../utils/JWT";
declare global {
namespace Express {
interface Request {
user?: JwtPayloadType;
}
}
}
+27
View File
@@ -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;
+12
View File
@@ -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);
}
}
}
+13
View File
@@ -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;
+66
View File
@@ -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<ICloudinaryResponse | undefined> => {
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;
+102
View File
@@ -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: `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Welcome Email</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Fallback styles for unsupported clients (some email clients ignore <style> tags) */
@media only screen and (max-width: 600px) {
.container {
padding: 20px !important;
}
.btn {
padding: 12px 18px !important;
font-size: 16px !important;
}
}
</style>
</head>
<body
style="margin: 0; padding: 0; font-family: Arial, sans-serif;">
<div style="max-width: 600px; margin: 40px auto; background-color: #f4f4f4; padding: 40px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.05);"
class="container">
<div style="font-size: 16px; color: #555555; line-height: 1.6;">
<p style="margin-bottom: 30px;">Hi <strong>${payload?.name || ""}</strong>,</p>
${payload?.htmlBody}
<div
style=" margin-top: 60px; text-align: center;">
<img style="width: 50px; height: 50px; border-radius: 50%;"
src="https://imgs.search.brave.com/IZoN38NQxnIIuB1I9E70bW6q5OvbEtz68YaxTe1j-o0/rs:fit:860:0:0:0/g:ce/aHR0cHM6Ly9lbGVt/ZW50cy1yZXNpemVk/LmVudmF0b3VzZXJj/b250ZW50LmNvbS9l/bGVtZW50cy1jb3Zl/ci1pbWFnZXMvMjhi/NmVjMTQtMGMwOS00/NGY1LWE5NGUtNmIy/OTM5NTZkMDM2P3c9/NDMzJmNmX2ZpdD1z/Y2FsZS1kb3duJnE9/ODUmZm9ybWF0PWF1/dG8mcz04Mjc0OWYy/ZDUyMmJiM2NlMjNi/OWNhNjhlZmFhNjdk/MTg5OGI4NWIwNzBh/MjQ1NjM4NmI1ZmFj/NWVmNmM5ZTNl"
alt="">
<p style="font-size: 12px;">The Support Team</p>
<h3>Company Name</h3>
</div>
</div>
<p style="font-size: 14px; color: #999999; margin-top: 20px; margin-bottom: 10px; text-align: center;">
This is an automated message — please do not reply to this email.
<br>
If you need assistance, feel free to contact our support team.
<br><br>
Thank you for choosing us!
</p>
<hr>
<div style="text-align: center; font-size: 12px; color: #999999; margin-top: 20px;">
&copy; {{year}} Your Company. All rights reserved.
</div>
</div>
</body>
</html>
`,
});
return info
};
export default sendMail;
+24
View File
@@ -0,0 +1,24 @@
import { Response } from "express"
interface IResponse<T> {
success: boolean,
statusCode: number,
message: string,
data?: T,
meta?: {
page?: number,
limit?: number,
skip?: number,
total?: number
}
}
const manageResponse = <T>(res: Response, payload: IResponse<T>) => {
res.status(payload.statusCode).json({
success: payload.success,
message: payload.message,
data: payload.data || undefined || null,
meta: payload.meta || undefined || null
})
}
export default manageResponse
+29
View File
@@ -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;
};
+11
View File
@@ -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;
+19
View File
@@ -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);
});
+48
View File
@@ -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",
),
],
};
+23
View File
@@ -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"
]
}