init: init project
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
Dockerfile
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
|
.env
|
||||||
|
dist
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
node_modules
|
||||||
|
.env
|
||||||
|
.env.dev
|
||||||
|
.env.example
|
||||||
|
.env.prod
|
||||||
|
package-lock.json
|
||||||
|
prisma/generated/
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"database": "prisma"
|
||||||
|
}
|
||||||
+27
@@ -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"]
|
||||||
@@ -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:
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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"
|
||||||
@@ -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?
|
||||||
|
}
|
||||||
@@ -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?
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
generator client {
|
||||||
|
provider = "prisma-client"
|
||||||
|
output = "../generated/prisma"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
# ⚡ Express Server CLI
|
||||||
|
|
||||||
|
[](https://www.npmjs.com/package/exp-node-server)
|
||||||
|
[](https://www.npmjs.com/package/exp-node-server)
|
||||||
|
[](https://www.npmjs.com/package/exp-node-server)
|
||||||
|
[](https://www.typescriptlang.org/)
|
||||||
|
[](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
@@ -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;
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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
|
||||||
@@ -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 };
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
@@ -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
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
export type TErrorSources = {
|
||||||
|
path: string | number
|
||||||
|
message: string
|
||||||
|
}[]
|
||||||
|
|
||||||
|
export type TGenericErrorResponse = {
|
||||||
|
statusCode: number
|
||||||
|
message: string
|
||||||
|
errorSources: TErrorSources
|
||||||
|
}
|
||||||
Vendored
+12
@@ -0,0 +1,12 @@
|
|||||||
|
import { TJwtUser } from "../modules/auth/auth.interface";
|
||||||
|
import { JwtPayloadType } from "../utils/JWT";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace Express {
|
||||||
|
interface Request {
|
||||||
|
user?: JwtPayloadType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;">
|
||||||
|
© {{year}} Your Company. All rights reserved.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
return info
|
||||||
|
};
|
||||||
|
|
||||||
|
export default sendMail;
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
@@ -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",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user