Compare commits

...

12 Commits

Author SHA1 Message Date
abumahid 46401bef0f update 2026-05-23 20:12:09 +06:00
abumahid 61fd639faf feat(account, order, plan, profile, redis): enhance functionality and security
- Updated CORS settings for frontend compatibility.
- Integrated Redis URL configuration.
- Improved login response structure in account service.
- Added role-based authorization for order and plan management.
- Enhanced error handling and logging in profile and plan services.
- Updated Swagger documentation for clarity on order statuses.
- Configured Redis connection for better performance.
2026-04-26 19:14:37 +06:00
abumahid 2d54031c33 🔧 refactor(plan): update imports to use .js extensions and secure delete route with admin auth 2026-04-26 19:10:56 +06:00
abumahid f886c392aa feat(account, queues): enhance email sending and Redis connection settings
- Added attempts and removal options for email queue tasks in `account.service.ts`.
- Updated Redis connection parameters to specify `maxRetriesPerRequest` and disable `enableReadyCheck` in `connection.ts`.
- Introduced an empty line for clarity in the `email.queue.ts` file.
2026-04-26 19:04:34 +06:00
sharafat 107b94bc97 adding the order service delete on the plan api 2026-04-26 18:57:33 +06:00
abumahid e227c42f7d feat(account): update login response and modify CORS origin
- Changed the CORS origin from `http://localhost:3000` to `http://localhost:5173`.
- Updated the login response to return a comprehensive object containing the `accessToken` and user profile data.
- Modified cookie setup to directly use `result.accessToken` instead of `result`.
- Refactored account service to include additional user fields in the login data returned.
- Added `dist` to `.gitignore`.
2026-04-26 18:56:50 +06:00
abumahid 0f7af70b90 ♻️ refactor(account, order, plan, profile, support, email): restructure application modules and enhance error handling
Updated Docker configuration, refactored middleware for improved error handling, and restructured account, order, plan, profile, and support modules, including their routes, services, and validations. Enhanced email processing queues and utilities for token generation, pagination, and response management to streamline the application architecture and enhance maintainability.
2026-04-21 03:12:39 +06:00
abumahid c881efea0f Merge remote-tracking branch 'origin/sharafat' into dev 2026-04-20 20:39:16 +06:00
sharafat 4c1614601a Order API:All routes was created and fully tested 2026-04-19 00:26:50 +06:00
sharafat 1bc1fae274 order api:implement pagination system 2026-04-17 23:36:01 +06:00
abumahid 739e3d1ad6 merge rahat 2026-04-15 23:38:15 +06:00
rahat0078 ba04c54c5b feat(support): complete full CRUD for support module 2026-04-13 00:35:51 +06:00
50 changed files with 894 additions and 211 deletions
+5 -4
View File
@@ -1,8 +1,9 @@
node_modules
npm-debug.log
Dockerfile
dist
.git
.gitignore
README.md
Dockerfile
docker-compose.yml
*.log
.env
dist
uploads
+1
View File
@@ -5,3 +5,4 @@ node_modules
.env.prod
package-lock.json
prisma/generated/
dist
+20 -5
View File
@@ -1,25 +1,40 @@
# ---------- BUILD STAGE ----------
FROM node:18-alpine AS builder
FROM node:20-alpine AS builder
WORKDIR /app
# Only install deps first (cache friendly)
COPY package*.json ./
RUN npm install
RUN npm ci
# Copy source
COPY . .
# Generate prisma + build
RUN npx prisma generate
RUN npm run build
# ---------- PRODUCTION STAGE ----------
FROM node:18-alpine
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install && npm cache clean --force
ENV NODE_ENV=production
# Only install production deps
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
# Copy Prisma generated client + schema
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma
COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma
COPY --from=builder /app/prisma ./prisma
# Copy built app
COPY --from=builder /app/dist ./dist
# Uploads folder
RUN mkdir -p /app/uploads
EXPOSE 5000
+1
View File
@@ -29,6 +29,7 @@
"pg": "^8.20.0",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"typescript": "^6.0.3",
"zod": "^4.1.12"
},
"devDependencies": {
@@ -0,0 +1,30 @@
-- CreateEnum
CREATE TYPE "T_SupportType" AS ENUM ('TECHNICAL', 'BILLING', 'DOMAIN', 'TEMPLATE', 'PAYMENT', 'ACCOUNT', 'FEATURE_REQUEST', 'BUG', 'OTHER');
-- CreateEnum
CREATE TYPE "T_SupportStatus" AS ENUM ('OPEN', 'IN_PROGRESS', 'RESOLVED', 'CLOSED');
-- CreateTable
CREATE TABLE "Support" (
"id" TEXT NOT NULL,
"issueName" TEXT NOT NULL,
"description" TEXT NOT NULL,
"type" "T_SupportType" NOT NULL,
"status" "T_SupportStatus" NOT NULL DEFAULT 'OPEN',
"resolvedBy" TEXT,
"resolvedAt" TIMESTAMP(3),
"storeAccountId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Support_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "Support_storeAccountId_idx" ON "Support"("storeAccountId");
-- CreateIndex
CREATE INDEX "Support_storeAccountId_status_idx" ON "Support"("storeAccountId", "status");
-- AddForeignKey
ALTER TABLE "Support" ADD CONSTRAINT "Support_storeAccountId_fkey" FOREIGN KEY ("storeAccountId") REFERENCES "Account"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,16 @@
/*
Warnings:
- The values [CLOSED] on the enum `T_SupportStatus` will be removed. If these variants are still used in the database, this will fail.
*/
-- AlterEnum
BEGIN;
CREATE TYPE "T_SupportStatus_new" AS ENUM ('OPEN', 'IN_PROGRESS', 'RESOLVED', 'REJECTED');
ALTER TABLE "public"."Support" ALTER COLUMN "status" DROP DEFAULT;
ALTER TABLE "Support" ALTER COLUMN "status" TYPE "T_SupportStatus_new" USING ("status"::text::"T_SupportStatus_new");
ALTER TYPE "T_SupportStatus" RENAME TO "T_SupportStatus_old";
ALTER TYPE "T_SupportStatus_new" RENAME TO "T_SupportStatus";
DROP TYPE "public"."T_SupportStatus_old";
ALTER TABLE "Support" ALTER COLUMN "status" SET DEFAULT 'OPEN';
COMMIT;
+1
View File
@@ -21,4 +21,5 @@ model Account {
profile Profile? //one-to-one
orders Order[] //one-to-many
supports Support[]
}
+2 -1
View File
@@ -2,7 +2,6 @@ model Profile {
id String @id @default(uuid())
accountId String @unique
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
shopName String
shopLogo String?
contactNumber String?
@@ -11,3 +10,5 @@ model Profile {
shopCategory String?
}
+1 -2
View File
@@ -1,6 +1,5 @@
generator client {
provider = "prisma-client"
output = "../generated/prisma"
provider = "prisma-client-js"
}
datasource db {
+43
View File
@@ -0,0 +1,43 @@
enum T_SupportType {
TECHNICAL
BILLING
DOMAIN
TEMPLATE
PAYMENT
ACCOUNT
FEATURE_REQUEST
BUG
OTHER
}
enum T_SupportStatus {
OPEN
IN_PROGRESS
RESOLVED
REJECTED
}
model Support {
id String @id @default(uuid())
issueName String
description String
type T_SupportType
status T_SupportStatus @default(OPEN)
resolvedBy String?
resolvedAt DateTime?
storeAccountId String
storeAccount Account @relation(fields: [storeAccountId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([storeAccountId])
@@index([storeAccountId, status])
}
+5 -5
View File
@@ -3,10 +3,10 @@ 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';
import globalErrorHandler from './app/middlewares/global_error_handler.js';
import notFound from './app/middlewares/not_found_api.js';
import appRouter from './routes.js';
import { swaggerOptions } from './swaggerOptions.js';
// define app
const app = express()
@@ -15,7 +15,7 @@ app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec));
// middleware
app.use(cors({
origin: ["http://localhost:3000"],
origin: ["http://localhost:5173","https://quick-launch-techzaa.vercel.app"],
methods: ["GET", "POST", "PATCH", "DELETE", "PUT"],
credentials: true
}))
+1
View File
@@ -23,4 +23,5 @@ export const configs = {
cloud_api_key: process.env.CLOUD_API_KEY,
cloud_api_secret: process.env.CLOUD_API_SECRET,
},
redis_url: process.env.REDIS_URL,
};
+1 -1
View File
@@ -1,5 +1,5 @@
import { ZodError, ZodIssue } from 'zod'
import { TErrorSources, TGenericErrorResponse } from '../types/error'
import { TErrorSources, TGenericErrorResponse } from '../types/error.js'
const handleZodError = (err: ZodError): TGenericErrorResponse => {
const errorSources: TErrorSources = err.issues.map((issue: ZodIssue) => {
+2 -1
View File
@@ -1,6 +1,7 @@
import { PrismaPg } from "@prisma/adapter-pg";
import pkg from "@prisma/client";
import "dotenv/config";
import { PrismaClient } from "../../../prisma/generated/prisma/client";
const { PrismaClient } = pkg;
const connectionString = `${process.env.DATABASE_URL}`;
+3 -3
View File
@@ -1,7 +1,7 @@
import { NextFunction, Request, Response } from "express";
import { configs } from "../configs";
import { AppError } from "../utils/app_error";
import { jwtHelpers, JwtPayloadType } from "../utils/JWT";
import { configs } from "../configs/index.js";
import { AppError } from "../utils/app_error.js";
import { jwtHelpers, JwtPayloadType } from "../utils/JWT.js";
type Role = "ADMIN" | "USER";
+4 -4
View File
@@ -1,9 +1,9 @@
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";
import { configs } from "../configs/index.js";
import handleZodError from "../errors/zodError.js";
import { TErrorSources } from "../types/error.js";
import { AppError } from "../utils/app_error.js";
const globalErrorHandler: ErrorRequestHandler = (err, req, res, next) => {
let statusCode = 500;
@@ -1,7 +1,7 @@
import { configs } from "../../configs";
import catchAsync from "../../utils/catch_async";
import manageResponse from "../../utils/manage_response";
import { account_services } from "./account.service";
import { configs } from "../../configs/index.js";
import catchAsync from "../../utils/catch_async.js";
import manageResponse from "../../utils/manage_response.js";
import { account_services } from "./account.service.js";
const create_account = catchAsync(async (req, res) => {
const result = await account_services.create_account_into_db(req);
@@ -37,7 +37,7 @@ 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, {
res.cookie("access_token", result.accessToken, {
secure: configs.env === "production",
httpOnly: true,
});
@@ -46,9 +46,7 @@ const login_user = catchAsync(async (req, res) => {
statusCode: 200,
success: true,
message: "User logged in successfully",
data: {
accessToken: result,
},
data: result,
});
});
const get_user_account = catchAsync(async (req, res) => {
+4 -4
View File
@@ -1,8 +1,8 @@
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";
import auth from "../../middlewares/auth.js";
import RequestValidator from "../../middlewares/request_validator.js";
import { account_controller } from "./account.controller.js";
import { account_validation } from "./account.validation.js";
const accountRouter = Router();
+36 -9
View File
@@ -1,12 +1,12 @@
import bcrypt from "bcrypt";
import { Request } from "express";
import { configs } from "../../configs";
import { prisma } from "../../lib/prisma";
import { emailQueue } from "../../queues/email/email.queue";
import { AppError } from "../../utils/app_error";
import { jwtHelpers } from "../../utils/JWT";
import sendMail from "../../utils/mail_sender";
import { otpGenerator } from "../../utils/otpGenerator";
import { configs } from "../../configs/index.js";
import { prisma } from "../../lib/prisma.js";
import { emailQueue } from "../../queues/email/email.queue.js";
import { AppError } from "../../utils/app_error.js";
import { jwtHelpers } from "../../utils/JWT.js";
import sendMail from "../../utils/mail_sender.js";
import { otpGenerator } from "../../utils/otpGenerator.js";
const create_account_into_db = async (req: Request) => {
const payload = req?.body;
@@ -22,7 +22,7 @@ const create_account_into_db = async (req: Request) => {
const hashPassword = bcrypt.hashSync(payload.password, 10);
// create account and profile
const result = await prisma.$transaction(async (tx) => {
const result = await prisma.$transaction(async (tx: any) => {
const account = await tx.account.create({
data: {
email: payload.email,
@@ -71,6 +71,10 @@ const create_account_into_db = async (req: Request) => {
subject: "Welcome to Quick Launch - Verification OTP",
email: payload.email,
textBody: "You can use otp or verification link for verifying your account"
}, {
attempts: 1,
removeOnComplete: true,
removeOnFail: true,
})
return null;
};
@@ -160,6 +164,15 @@ const login_user_into_db = async (req: Request) => {
where: {
email: payload.email,
},
select: {
id: true,
email: true,
role: true,
isAccountVerified: true,
isDeleted: true,
password: true,
profile: true,
},
});
// check if account exists
@@ -196,7 +209,21 @@ const login_user_into_db = async (req: Request) => {
configs.jwt.access_token as string,
configs.jwt.access_expires as string,
);
return accessToken;
const finalOutputData = {
id: account.id,
email: account.email,
role: account.role,
shopName: account?.profile?.shopName,
shopLogo: account?.profile?.shopLogo,
}
return {
accessToken,
profile: finalOutputData
};
};
const get_user_account_from_db = async (req: Request) => {
+3 -3
View File
@@ -1,7 +1,7 @@
import catchAsync from "../../utils/catch_async";
import manageResponse from "../../utils/manage_response";
import { order_service } from "./order.service";
import catchAsync from "../../utils/catch_async.js";
import manageResponse from "../../utils/manage_response.js";
import { order_service } from "./order.service.js";
const get_all_order = catchAsync(async (req, res) => {
const result = await order_service.get_all_order_from_db(req);
+5 -4
View File
@@ -1,8 +1,9 @@
import { Router } from "express";
import RequestValidator from "../../middlewares/request_validator";
import { order_controller } from "./order.controller";
import { order_validations } from "./order.validation";
import RequestValidator from "../../middlewares/request_validator.js";
import { order_controller } from "./order.controller.js";
import { order_validations } from "./order.validation.js";
import auth from "../../middlewares/auth.js";
const router = Router();
@@ -14,7 +15,7 @@ router.post(
);
router.get("/:id", order_controller.get_single_order);
router.patch(
"/:id",
"/:id",auth("ADMIN"),
RequestValidator(order_validations.update_order),
order_controller.update_order,
);
+105 -37
View File
@@ -1,27 +1,36 @@
import { Request } from "express";
import { configs } from "../../configs";
import { prisma } from "../../lib/prisma";
import { orderEmailQueue } from "../../queues/email/order/order.email.queue";
import { configs } from "../../configs/index.js";
import { prisma } from "../../lib/prisma.js";
import { orderEmailQueue } from "../../queues/email/order/order.email.queue.js";
import { AppError } from "../../utils/app_error.js";
import paginationHelper from "../../utils/pagination_helper.js";
const get_all_order_from_db = async (req: Request) => {
// define your own login here
const search=req.query.search as string
const customerName=req.query.customerName as string
const productName=req.query.productName as string
console.log(productName)
const andCondition=[] as any[]
const search = req.query.search as string;
const customerName = req.query.customerName as string;
const productName = req.query.productName as string;
// for date filter
const startDate = req.query.startDate as string;
const endDate = req.query.endDate as string;
const status = (req.query.status as string) || undefined;
const { page, limit, skip, sortBy, sortOrder } = paginationHelper(req.query);
const andCondition = [] as any[];
if (search) {
andCondition.push({
OR: [
{
productName: {
contains: search,
mode:"insensitive"
}
}
]
})
mode: "insensitive",
},
},
],
});
}
if (customerName) {
andCondition.push({
@@ -29,11 +38,11 @@ const get_all_order_from_db = async (req: Request) => {
{
customerName: {
contains: customerName,
mode:"insensitive"
}
}
]
})
mode: "insensitive",
},
},
],
});
}
if (productName) {
andCondition.push({
@@ -41,21 +50,67 @@ const get_all_order_from_db = async (req: Request) => {
{
productName: {
contains: productName,
mode:"insensitive"
}
}
]
})
}
console.log(search)
const result = await prisma.order.findMany({
where:{
AND:andCondition
}
mode: "insensitive",
},
},
],
});
return result;
}
if (status) {
andCondition.push({
OR: [
{
status: {
contains: status,
mode: "insensitive",
},
},
],
});
}
// for date filter
const dateFilter: any = {};
if (startDate) {
const start = new Date(startDate);
start.setHours(0, 0, 0, 0);
dateFilter.gte = start;
}
if (endDate) {
const end = new Date(endDate);
end.setHours(23, 59, 59, 999);
dateFilter.lte = end;
}
if (Object.keys(dateFilter).length > 0) {
andCondition.push({
createdAt: dateFilter,
});
}
const getAllOrders = await prisma.order.findMany({
take: limit,
skip,
where: {
AND: andCondition,
},
orderBy: {
[sortBy as string]: sortOrder,
},
});
const result = await prisma.order.count({
where: {
AND: andCondition,
},
});
return {
data: getAllOrders,
pagination: {
total: result,
page,
limit,
totalPages: Math.ceil(result / limit),
},
};
};
const get_single_order_from_db = async (req: Request) => {
@@ -67,9 +122,9 @@ const get_single_order_from_db = async (req: Request) => {
const create_order_into_db = async (req: Request) => {
const payload = req?.body;
console.log(payload)
console.log(payload);
payload.status = "INITIATED";
payload.paymentType = "COD"
payload.paymentType = "COD";
// nwo init order
const result = await prisma.order.create({ data: payload });
@@ -81,15 +136,24 @@ const create_order_into_db = async (req: Request) => {
email: payload.customerEmail,
subject: "Order Tracking",
textBody: `Your order has been created. Track your order here: ${trackingLink}`,
htmlBody: `<p>Your order has been created. Track your order here: <a href="${trackingLink}">Track Order</a></p>`
})
htmlBody: `<p>Your order has been created. Track your order here: <a href="${trackingLink}">Track Order</a></p>`,
});
}
return result;
};
const update_order_into_db = async (req: Request) => {
// define your own login here
const user = req.user;
console.log(user);
if (user?.role !== "ADMIN") {
throw new AppError("You are not authorized to perform this action", 403);
}
const { id } = req.params as { id: string };
const isProductExist = await prisma.order.findUnique({ where: { id } });
if (!isProductExist) {
throw new AppError("Order is not found", 404);
}
const result = await prisma.order.update({ where: { id }, data: req.body });
return result;
};
@@ -97,6 +161,10 @@ const update_order_into_db = async (req: Request) => {
const delete_order_from_db = async (req: Request) => {
// define your own login here
const { id } = req.params as { id: string };
const user = req.user;
if (user?.role !== "ADMIN") {
throw new AppError("You are not authorized to perform this action", 403);
}
const result = await prisma.order.delete({ where: { id } });
return result;
};
+43 -23
View File
@@ -1,25 +1,28 @@
export const orderSwaggerDocs = {
"/api/order": {
post: {
tags: ["order"],
summary: "Create new order",
description: "",
description: ` INITIATED
CONFIRMED
ONGOING
DELIVERED
CANCELLED`,
requestBody: {
required: true,
content: {
"application/json": {
example: JSON.stringify({
"shopAccountId": "",
"productPrice": 1500,
"productQuantity": 2,
"productName": "Wireless Mouse",
"customerName": "Rahim Uddin",
"customerPhone": "+8801712345678",
"customerEmail": "softvence.abumahid@gmail.com",
"customerAddress": "Rangpur, Bangladesh",
"customerNote": "Please deliver between 3-5 PM"
})
shopAccountId: "",
productPrice: 1500,
productQuantity: 2,
productName: "Wireless Mouse",
customerName: "Rahim Uddin",
customerPhone: "+8801712345678",
customerEmail: "softvence.abumahid@gmail.com",
customerAddress: "Rangpur, Bangladesh",
customerNote: "Please deliver between 3-5 PM",
}),
},
},
},
@@ -63,6 +66,32 @@ export const orderSwaggerDocs = {
required: false,
schema: { type: "string" },
},
{
name: "status",
in: "query",
required: false,
schema: { type: "string" },
},
{
name: "date",
in: "query",
required: false,
schema: { type: "string" },
},
{
name: "startDate",
in: "query",
required: false,
schema: { type: "string", format: "date" },
example: "2026-04-01",
},
{
name: "endDate",
in: "query",
required: false,
schema: { type: "string", format: "date" },
example: "2026-04-31",
},
],
responses: {
200: { description: "order fetched successfully" },
@@ -91,7 +120,7 @@ export const orderSwaggerDocs = {
},
patch: {
tags: ["order"],
summary: "Update order",
summary: "Update order -(Admin route)",
description: "",
parameters: [
{
@@ -106,14 +135,7 @@ export const orderSwaggerDocs = {
content: {
"application/json": {
example: JSON.stringify({
"shopAccountId": "",
"productPrice": 1500,
"productQuantity": 2,
"productName": "Wireless Mouse",
"customerName": "Rahim Uddin",
"customerPhone": "+8801712345678",
"customerAddress": "Rangpur, Bangladesh",
"customerNote": "Please deliver between 3-5 PM"
status: "INITIATED",
}), // put your request body
},
},
@@ -142,5 +164,3 @@ export const orderSwaggerDocs = {
},
},
};
+3 -3
View File
@@ -1,7 +1,7 @@
import catchAsync from "../../utils/catch_async";
import manageResponse from "../../utils/manage_response";
import { plan_service } from "./plan.service";
import catchAsync from "../../utils/catch_async.js";
import manageResponse from "../../utils/manage_response.js";
import { plan_service } from "./plan.service.js";
const get_all_plan = catchAsync(async (req, res) => {
const result = await plan_service.get_all_plan_from_db(req);
+7 -6
View File
@@ -1,8 +1,8 @@
import { Router } from "express";
import RequestValidator from "../../middlewares/request_validator";
import { plan_controller } from "./plan.controller";
import { plan_validations } from "./plan.validation";
import auth from "../../middlewares/auth.js";
import RequestValidator from "../../middlewares/request_validator.js";
import { plan_controller } from "./plan.controller.js";
import { plan_validations } from "./plan.validation.js";
const router = Router();
@@ -10,15 +10,16 @@ router.get("/", plan_controller.get_all_plan);
router.post(
"/",
RequestValidator(plan_validations.create_plan),
auth("ADMIN"),
plan_controller.create_plan,
);
router.get("/:id", plan_controller.get_single_plan);
router.patch(
"/:id",
RequestValidator(plan_validations.update_plan),
auth("ADMIN"),
plan_controller.update_plan,
);
router.delete("/:id", plan_controller.delete_plan);
router.delete("/:id", auth("ADMIN"), plan_controller.delete_plan);
export default router;
+3 -3
View File
@@ -1,7 +1,7 @@
import { Request } from "express";
import { prisma } from "../../lib/prisma";
import { AppError } from "../../utils/app_error";
import { prisma } from "../../lib/prisma.js";
import { AppError } from "../../utils/app_error.js";
const get_all_plan_from_db = async (req: Request) => {
// define your own login here
@@ -22,7 +22,7 @@ const get_single_plan_from_db = async (req: Request) => {
const create_plan_into_db = async (req: Request) => {
// define your own login here
const user = req.user
const user = req?.user
if (user?.role !== "ADMIN") {
throw new AppError("You dont have permission to create plan information.!!!", 401)
}
@@ -1,6 +1,6 @@
import catchAsync from "../../utils/catch_async";
import manageResponse from "../../utils/manage_response";
import { profile_service } from "./profile.service";
import catchAsync from "../../utils/catch_async.js";
import manageResponse from "../../utils/manage_response.js";
import { profile_service } from "./profile.service.js";
const update_profile = catchAsync(async (req, res) => {
const result = await profile_service.update_profile_into_db(req);
+5 -5
View File
@@ -1,9 +1,9 @@
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";
import RequestValidator from "../../middlewares/request_validator.js";
import { profile_controller } from "./profile.controller.js";
import { profile_validations } from "./profile.validation.js";
import auth from "../../middlewares/auth.js";
import uploader from "../../middlewares/uploader.js";
const router = Router();
+4 -3
View File
@@ -1,12 +1,13 @@
import { Request } from "express";
import uploadCloud from "../../utils/cloudinary";
import { prisma } from "../../lib/prisma";
import { JwtPayloadType } from "../../utils/JWT";
import uploadCloud from "../../utils/cloudinary.js";
import { prisma } from "../../lib/prisma.js";
import { JwtPayloadType } from "../../utils/JWT.js";
const update_profile_into_db = async (req: Request) => {
const user = req?.user as JwtPayloadType;
const payload = req?.body;
const file = req?.file;
console.log(payload);
// check file and upload to cloud
if (file) {
const cloudRes = await uploadCloud(file);
@@ -0,0 +1,99 @@
import { Request, Response } from "express";
import catchAsync from "../../utils/catch_async.js";
import manageResponse from "../../utils/manage_response.js";
import { support_service } from "./support.service.js";
const createSupport = catchAsync(async (req: Request, res: Response) => {
const id = req?.user?.accountId;
const data = {
...req.body,
storeAccountId: id as string
}
const result = await support_service.createSupportIntoDB(data);
manageResponse(res, {
success: true,
statusCode: 200,
message: "support created successfully.",
data: result,
meta: {},
});
});
const getAllSupport = catchAsync(async (req: Request, res: Response) => {
const role = req?.user?.role;
const id = req?.user?.accountId;
const search = req?.query?.search;
const type = req?.query?.type;
const status = req?.query?.status;
const result = await support_service.getAllSupportFromDB(id as string, role as string, search as string, type as string, status as string);
manageResponse(res, {
success: true,
statusCode: 200,
message: "All support fetched successfully.",
data: result,
meta: {},
});
});
const get_single_support = catchAsync(async (req, res) => {
const {id} = req.params;
const userId = req?.user?.accountId;
const role = req?.user?.role
const result = await support_service.getSingleSupportFromDB(id as string, userId as string, role as string);
manageResponse(res, {
success: true,
statusCode: 200,
message: "Single support fetched successfully.",
data: result,
meta: {},
});
});
const update_support = catchAsync(async (req, res) => {
const {id} = req.params;
const userId = req?.user?.accountId;
const role = req?.user?.role;
const data = req.body
const result = await support_service.updateSupportIntoDB(id as string, userId as string, role as string, data);
manageResponse(res, {
success: true,
statusCode: 200,
message: "support updated successfully.",
data: result,
meta: {},
});
});
const delete_support = catchAsync(async (req, res) => {
const {id} = req.params;
const userId = req?.user?.accountId;
const role = req?.user?.role
const result = await support_service.deleteSupportFromDB(id as string, userId as string, role as string);
manageResponse(res, {
success: true,
statusCode: 200,
message: "support deleted successfully.",
data: result,
meta: {},
});
});
export const support_controller = {
createSupport,
getAllSupport,
get_single_support,
update_support,
delete_support,
};
+30
View File
@@ -0,0 +1,30 @@
import { Router } from "express";
import RequestValidator from "../../middlewares/request_validator.js";
import { support_controller } from "./support.controller.js";
import { support_validations } from "./support.validation.js";
import auth from "../../middlewares/auth.js";
const router = Router();
router.get("/", auth("ADMIN", "USER"), support_controller.getAllSupport);
router.post(
"/",
auth("ADMIN", "USER"),
RequestValidator(support_validations.create_support),
support_controller.createSupport,
);
router.get("/:id", auth("ADMIN", "USER"), support_controller.get_single_support);
router.patch(
"/:id",
auth("ADMIN", "USER"),
RequestValidator(support_validations.update_support),
support_controller.update_support,
);
router.delete("/:id", auth("ADMIN", "USER"), support_controller.delete_support);
export default router;
+139
View File
@@ -0,0 +1,139 @@
import { prisma } from "../../lib/prisma.js";
import { Prisma } from "@prisma/client";
import { AppError } from "../../utils/app_error.js";
const createSupportIntoDB = async (payload: any) => {
const result = await prisma.support.create({ data: payload });
return result;
};
const getAllSupportFromDB = async (
user_id: string,
role: string,
search?: string,
type?: string,
status?: string,
) => {
const andCondition: Prisma.SupportWhereInput[] = [];
if (search) {
andCondition.push({
OR: [
{
issueName: {
contains: search,
mode: "insensitive",
},
},
{
description: {
contains: search,
mode: "insensitive",
},
},
],
});
}
if (type) {
andCondition.push({
type: type as any,
});
}
if (status) {
andCondition.push({
status: status as any,
});
}
if (role !== "ADMIN") {
andCondition.push({
storeAccountId: user_id,
});
}
const whereCondition: Prisma.SupportWhereInput =
andCondition.length > 0 ? { AND: andCondition } : {};
const result = await prisma.support.findMany({
where: whereCondition,
orderBy: {
createdAt: "desc",
},
});
return result;
};
const getSingleSupportFromDB = async (
id: string,
userId: string,
role: string,
) => {
const support = await prisma.support.findUnique({
where: { id },
});
if (!support) {
throw new AppError("Support not found", 404);
}
if (role !== "ADMIN" && support.storeAccountId !== userId) {
throw new AppError("You are not authorized", 403);
}
return support;
};
const updateSupportIntoDB = async (
id: string,
userId: string,
role: string,
payload: any,
) => {
const support = await prisma.support.findUnique({
where: { id },
});
if (!support) {
throw new AppError("Support not found", 404);
}
if (role !== "ADMIN" && support.storeAccountId !== userId) {
throw new AppError("You are not authorized", 403);
}
const result = await prisma.support.update({
where: { id },
data: payload,
});
return result;
};
const deleteSupportFromDB = async (
id: string,
userId: string,
role: string,
) => {
const support = await prisma.support.findUnique({
where: { id },
});
if (!support) {
throw new AppError("Support not found", 404);
}
if (role !== "ADMIN" && support.storeAccountId !== userId) {
throw new AppError("You are not authorized", 403);
}
const result = await prisma.support.delete({
where: {id}
})
return result;
};
export const support_service = {
createSupportIntoDB,
getAllSupportFromDB,
getSingleSupportFromDB,
updateSupportIntoDB,
deleteSupportFromDB,
};
+114
View File
@@ -0,0 +1,114 @@
export const supportSwaggerDocs = {
"/api/support": {
post: {
tags: ["support"],
summary: "Create new support",
description: "",
requestBody: {
required: true,
content: {
"application/json": {
example: JSON.stringify({
"issueName": "Your issue name",
"description": "Issue description",
"type": "Issue Type"
}), // put your request body
},
},
},
responses: {
201: { description: "support created successfully" },
500: { description: "Validation error or internal server error" },
},
},
get: {
tags: ["support"],
summary: "Get all support",
description: "",
parameters: [
{
name: "page",
in: "query",
required: false,
schema: { type: "number" },
},
{
name: "limit",
in: "query",
required: false,
schema: { type: "number" },
},
],
responses: {
200: { description: "support fetched successfully" },
401: { description: "unauthorized" },
},
},
},
"/api/support/{id}": {
get: {
tags: ["support"],
summary: "Get single support",
description: "",
parameters: [
{
name: "id",
in: "path",
required: true,
schema: { type: "string" },
},
],
responses: {
200: { description: "support fetched successfully" },
401: { description: "unauthorized" },
},
},
patch: {
tags: ["support"],
summary: "Update support",
description: "",
parameters: [
{
name: "id",
in: "path",
required: true,
schema: { type: "string" },
},
],
requestBody: {
required: true,
content: {
"application/json": {
example: JSON.stringify({}), // put your request body
},
},
},
responses: {
200: { description: "support updated successfully" },
500: { description: "Validation error or internal server error" },
},
},
delete: {
tags: ["support"],
summary: "Delete support",
description: "",
parameters: [
{
name: "id",
in: "path",
required: true,
schema: { type: "string" },
},
],
responses: {
200: { description: "support delete successfully" },
401: { description: "unauthorized" },
},
},
},
};
@@ -0,0 +1,33 @@
import { z } from "zod";
const create_support = z.object({
issueName: z.string().min(1, "issueName is required"),
description: z.string().min(1, "description is required"),
type: z.enum([
"TECHNICAL",
"BILLING",
"DOMAIN",
"TEMPLATE",
"PAYMENT",
"ACCOUNT",
"FEATURE_REQUEST",
"BUG",
"OTHER",
]),
});
const update_support = z.object({
issueName: z.string().optional(),
description: z.string().optional(),
type: z.enum(["BUG", "PAYMENT", "ACCOUNT", "OTHER"]).optional(),
status: z.enum(["OPEN", "IN_PROGRESS", "RESOLVED", "REJECTED"]).optional(),
resolvedBy: z.string().optional(),
resolvedAt: z.coerce.date().optional(),
});
export const support_validations = {
create_support,
update_support,
};
+7 -5
View File
@@ -1,6 +1,8 @@
import { QueueOptions } from "bullmq";
import { Redis } from "ioredis";
import { configs } from "../configs/index.js";
export const redisConnection: QueueOptions["connection"] = {
host: "127.0.0.1",
port: 6379,
};
export const redisConnection = new Redis(configs.redis_url as string, {
tls: {},
maxRetriesPerRequest: null,
enableReadyCheck: false,
});
+3 -3
View File
@@ -1,6 +1,6 @@
import { otpTemplate } from "../../templates/otpTemplate";
import sendMail from "../../utils/mail_sender";
import { TEmailQueue } from "./email.queue";
import { otpTemplate } from "../../templates/otpTemplate.js";
import sendMail from "../../utils/mail_sender.js";
import { TEmailQueue } from "./email.queue.js";
// email.processor.ts
export const emailProcessor = async (job: any) => {
+2 -1
View File
@@ -1,6 +1,6 @@
// email.queue.ts
import { Queue } from "bullmq";
import { redisConnection } from "../connection";
import { redisConnection } from "../connection.js";
export type TEmailQueue = {
email: string;
@@ -13,4 +13,5 @@ export type TEmailQueue = {
export const emailQueue = new Queue<TEmailQueue>("email-queue", {
connection: redisConnection,
});
+3 -3
View File
@@ -1,8 +1,8 @@
// email.worker.ts
import { Worker } from "bullmq";
import { redisConnection } from "../connection";
import { emailProcessor } from "./email.processor";
import { TEmailQueue } from "./email.queue";
import { redisConnection } from "../connection.js";
import { emailProcessor } from "./email.processor.js";
import { TEmailQueue } from "./email.queue.js";
export const emailWorker = new Worker<TEmailQueue>(
"email-queue",
@@ -1,5 +1,5 @@
import sendMail from "../../../utils/mail_sender";
import { TOrderEmailQueue } from "./order.email.queue";
import sendMail from "../../../utils/mail_sender.js";
import { TOrderEmailQueue } from "./order.email.queue.js";
// email.processor.ts
export const orderEmailProcessor = async (job: any) => {
@@ -1,5 +1,5 @@
import { Queue } from "bullmq";
import { redisConnection } from "../../connection";
import { redisConnection } from "../../connection.js";
export type TOrderEmailQueue = {
email: string;
@@ -1,8 +1,8 @@
// email.worker.ts
import { Worker } from "bullmq";
import { redisConnection } from "../../connection";
import { orderEmailProcessor } from "./order.email.processor";
import { TOrderEmailQueue } from "./order.email.queue";
import { redisConnection } from "../../connection.js";
import { orderEmailProcessor } from "./order.email.processor.js";
import { TOrderEmailQueue } from "./order.email.queue.js";
export const emailWorker = new Worker<TOrderEmailQueue>(
+2 -2
View File
@@ -1,4 +1,4 @@
import "./email/email.worker";
import "./email/order/order.email.worker";
import "./email/email.worker.js";
import "./email/order/order.email.worker.js";
console.log("Workers running...");
+1 -1
View File
@@ -1,4 +1,4 @@
import { TEmailQueue } from "../queues/email/email.queue"
import { TEmailQueue } from "../queues/email/email.queue.js"
export const otpTemplate = (payload: TEmailQueue) => {
return `
+1 -1
View File
@@ -1,6 +1,6 @@
import { v2 as cloudinary } from 'cloudinary';
import fs from 'fs';
import { configs } from '../configs';
import { configs } from '../configs/index.js';
type ICloudinaryResponse = {
asset_id: string;
+1 -1
View File
@@ -1,5 +1,5 @@
import nodemailer from 'nodemailer';
import { configs } from '../configs';
import { configs } from '../configs/index.js';
type TMailContent = {
to: string,
subject: string,
+34
View File
@@ -0,0 +1,34 @@
// options type
type IOptions = {
page?: number | string;
limit?: number | string;
sortOrder?: string;
sortBy?: string;
};
// return type
export type IPaginationResult = {
page: number;
limit: number;
skip: number;
sortOrder?: string;
sortBy?: string;
};
const paginationHelper = (options:IOptions): IPaginationResult => {
const page = Number(options?.page) || 1;
const limit = Number(options?.limit) || 10;
const skip = (page-1)*limit
const sortBy =options?.sortBy ||"createdAt";
const sortOrder = options?.sortOrder || "desc";
return {
page,
limit,
skip,
sortBy,
sortOrder
}
};
export default paginationHelper;
+9 -5
View File
@@ -1,15 +1,19 @@
import { Router } from "express";
import accountRouter from "./app/modules/account/account.route";
import profileRoute from "./app/modules/profile/profile.route";
import planRoute from "./app/modules/plan/plan.route";
import orderRoute from "./app/modules/order/order.route";
import accountRouter from "./app/modules/account/account.route.js";
import orderRoute from "./app/modules/order/order.route.js";
import planRoute from "./app/modules/plan/plan.route.js";
import profileRoute from "./app/modules/profile/profile.route.js";
import supportRoute from "./app/modules/support/support.route.js";
const appRouter = Router();
const moduleRoutes = [
{ path: "/order", route: orderRoute },
{ path: "/support", route: supportRoute },
{ path: "/plan", route: planRoute },
{ path: "/profile", route: profileRoute },{ path: "/auth", route: accountRouter }];
{ path: "/profile", route: profileRoute },
{ path: "/auth", route: accountRouter },
];
moduleRoutes.forEach((route) => appRouter.use(route.path, route.route));
export default appRouter;
+4 -4
View File
@@ -1,7 +1,7 @@
import app from "./app";
import { configs } from "./app/configs/index";
import { prisma } from "./app/lib/prisma";
import "./app/queues/worker";
import app from "./app.js";
import { configs } from "./app/configs/index.js";
import { prisma } from "./app/lib/prisma.js";
import "./app/queues/worker.js";
async function main() {
try {
+10 -8
View File
@@ -1,10 +1,11 @@
import { fileURLToPath } from "node:url";
import path from "path";
import { configs } from "./app/configs";
import { accountSwaggerDocs } from "./app/modules/account/account.swagger";
import { orderSwaggerDocs } from "./app/modules/order/order.swagger";
import { planSwaggerDocs } from "./app/modules/plan/plan.swagger";
import { profileSwaggerDocs } from "./app/modules/profile/profile.swagger";
import { configs } from "./app/configs/index.js";
import { accountSwaggerDocs } from "./app/modules/account/account.swagger.js";
import { orderSwaggerDocs } from "./app/modules/order/order.swagger.js";
import { planSwaggerDocs } from "./app/modules/plan/plan.swagger.js";
import { profileSwaggerDocs } from "./app/modules/profile/profile.swagger.js";
import { supportSwaggerDocs } from "./app/modules/support/support.swagger.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -19,14 +20,15 @@ export const swaggerOptions = {
},
paths: {
...accountSwaggerDocs,
...profileSwaggerDocs,
...planSwaggerDocs,
...orderSwaggerDocs,
...profileSwaggerDocs,
...supportSwaggerDocs,
},
servers:
configs.env === "production"
? [{ url: "https://live-url.com" }, { url: "http://localhost:5000" }]
: [{ url: "http://localhost:5000" }, { url: "https://live-url.com" }],
? [{ url: "https://quicklunch-server.onrender.com" }, { url: "http://localhost:5000" }]
: [{ url: "http://localhost:5000" }, { url: "https://quicklunch-server.onrender.com" }],
components: {
securitySchemes: {
AuthorizationToken: {
+4 -5
View File
@@ -1,9 +1,9 @@
{
"compilerOptions": {
"target": "ES2023",
"module": "ESNext",
"moduleResolution": "bundler",
"rootDir": "./",
"module": "nodenext",
"moduleResolution": "nodenext",
"rootDir": "./src",
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
@@ -11,8 +11,7 @@
"skipLibCheck": true
},
"include": [
"src/**/*",
"prisma/**/*"
"src/**/*"
],
"exclude": [
"node_modules",