From 45f35d97440d19d033e3415a154fa49bb86a458f Mon Sep 17 00:00:00 2001 From: akshad-exe Date: Thu, 6 Nov 2025 19:45:07 +0530 Subject: [PATCH 1/4] Fix CORS issues and add contact message status updates - Configure CORS to allow ngrok origins and handle preflight requests - Update authentication middleware to skip OPTIONS requests - Add contact message status update functionality - Fix database configuration validation --- prisma/schema.prisma | 13 +++-- src/app.ts | 36 ++++++++++++- src/config/database.ts | 52 ++++++++++++++++--- src/config/envVars.ts | 4 +- .../contactForm/contactForms.controller.ts | 52 ++++++++++++++++++- src/middlewares/auth.middleware.ts | 5 ++ src/routes/v1/admin/routes.ts | 1 + 7 files changed, 149 insertions(+), 14 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e59760c..cb2b53f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -17,13 +17,14 @@ model Waitlists { } model ContactUs { - id String @id @default(auto()) @map("_id") @db.ObjectId + id String @id @default(auto()) @map("_id") @db.ObjectId name String email String subject String message String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + status ContactStatus @default(UNREAD) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } enum WaitlistStatus { @@ -31,3 +32,9 @@ enum WaitlistStatus { VERIFIED REJECTED } + +enum ContactStatus { + UNREAD + READ + REPLIED +} diff --git a/src/app.ts b/src/app.ts index 6204826..cc141fb 100644 --- a/src/app.ts +++ b/src/app.ts @@ -7,9 +7,43 @@ import { errorConverter, errorHandler } from "./handlers/error.handler"; const app = express(); +// Configure CORS to handle preflight requests properly +const corsOptions = { + origin: function (origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) { + // Allow requests with no origin (like mobile apps or curl requests) + if (!origin) return callback(null, true); + + // Allow localhost for development (only exact localhost origins) + const localhostRegex = /^https?:\/\/localhost(:\d+)?$/; + if (origin && localhostRegex.test(origin)) return callback(null, true); + + // Allow ngrok tunnels (only valid ngrok.io subdomains) + const ngrokRegex = /^https?:\/\/([a-z0-9-]+)\.ngrok\.io$/; + if (origin && ngrokRegex.test(origin)) return callback(null, true); + + // Allow specific domains if needed + const allowedOrigins = [ + 'http://localhost:3000', + 'http://localhost:3030', + 'https://brickchain.in', + 'https://www.brickchain.in' + ]; + + if (allowedOrigins.includes(origin)) { + return callback(null, true); + } + + return callback(new Error('Not allowed by CORS')); + }, + credentials: true, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization', 'ngrok-skip-browser-warning'], + optionsSuccessStatus: 200 // Some legacy browsers choke on 204 +}; + app.use(express.json()); app.use(express.urlencoded({ extended: true })); -app.use(cors()); +app.use(cors(corsOptions)); app.use(morgan("dev")); app.use(helmet()); diff --git a/src/config/database.ts b/src/config/database.ts index 3d45280..2ea2501 100644 --- a/src/config/database.ts +++ b/src/config/database.ts @@ -1,18 +1,54 @@ import { PrismaClient } from "../../generated/prisma"; import { logger } from "./logger"; + interface CustomNodeJsGlobal extends Global { prisma: PrismaClient; } declare const global: CustomNodeJsGlobal; -export const db = global.prisma || new PrismaClient(); - -db.$connect() - .then(() => { - logger.info("[PRISMA] : connected to database"); - }) - .catch((error: string) => { - logger.error("[PRISMA] : failed to connect database : ", error); +// Create PrismaClient with logging enabled to help diagnose connection issues +export const db = + global.prisma || + new PrismaClient({ + log: ["info", "warn", "error"], }); + +// Track whether Prisma has successfully connected so other parts of the app +// can report DB health without attempting queries that would fail. +let _isDbConnected = false; +export function isDbConnected() { + return _isDbConnected; +} + +// Attempt to connect with a small retry/backoff loop to handle transient network issues. +async function connectWithRetry(maxRetries = 5, initialDelayMs = 1000) { + let attempt = 0; + let delay = initialDelayMs; + while (attempt < maxRetries) { + try { + await db.$connect(); + _isDbConnected = true; + logger.info("[PRISMA] : connected to database"); + return; + } catch (error: any) { + attempt += 1; + logger.warn( + `[PRISMA] : failed to connect (attempt ${attempt}/${maxRetries}):`, + error?.message ?? error, + ); + if (attempt >= maxRetries) { + logger.error("[PRISMA] : exhausted all connection retries.", error); + // leave _isDbConnected as false so health checks report DB down + return; + } + // exponential backoff + await new Promise((res) => setTimeout(res, delay)); + delay *= 2; + } + } +} + +// Start background connection attempts (do not block module initialization). +connectWithRetry().catch((err) => logger.error("[PRISMA] : connectWithRetry unexpected error:", err)); diff --git a/src/config/envVars.ts b/src/config/envVars.ts index b1e81da..250df5b 100644 --- a/src/config/envVars.ts +++ b/src/config/envVars.ts @@ -21,6 +21,7 @@ const EnvConfigSchema = z.object({ .default("development"), RESEND_API_KEY: z.string().min(1, { message: "RESEND_API_KEY is required" }), + DATABASE_URL: z.string().min(1, { message: "DATABASE_URL is required" }), JWT_SECRET: z.string().min(1, { message: "JWT_SECRET is required" }), }); export type EnvConfig = z.infer; @@ -29,6 +30,7 @@ const rawConfig = { PORT: process.env.PORT, NODE_ENV: process.env.NODE_ENV, RESEND_API_KEY: process.env.RESEND_API_KEY, + DATABASE_URL: process.env.DATABASE_URL, JWT_SECRET: process.env.JWT_SECRET, }; @@ -54,6 +56,6 @@ try { ); } -export const { PORT, NODE_ENV, RESEND_API_KEY, JWT_SECRET } = envVars; +export const { PORT, NODE_ENV, RESEND_API_KEY, DATABASE_URL, JWT_SECRET } = envVars; export default envVars; diff --git a/src/controllers/v1/admin/contactForm/contactForms.controller.ts b/src/controllers/v1/admin/contactForm/contactForms.controller.ts index 420ddfc..0033164 100644 --- a/src/controllers/v1/admin/contactForm/contactForms.controller.ts +++ b/src/controllers/v1/admin/contactForm/contactForms.controller.ts @@ -1,15 +1,65 @@ import { db } from "@/config/database"; import catchAsync from "@/handlers/async.handler"; +import { APIError } from "@/utils/APIError"; import { Request, Response } from "express"; +import { ContactStatus } from "generated/prisma"; const getAllContactForms = catchAsync(async (req: Request, res: Response) => { - const forms = await db.contactUs.findMany(); + const forms = await db.contactUs.findMany({ + orderBy: { + createdAt: 'desc' + } + }); res.status(200).json({ status: "success", data: forms, }); }); +const updateContactStatus = catchAsync(async (req: Request, res: Response) => { + const { id } = req.params; + const { status } = req.body; + + if (!id || id.trim() === "") { + throw new APIError(400, "Contact ID is required"); + } + + if (!status) { + throw new APIError(400, "Status is required"); + } + + const validStatuses = Object.values(ContactStatus); + if (!validStatuses.includes(status.toUpperCase())) { + throw new APIError(400, `Invalid status. Must be one of: ${validStatuses.join(', ')}`); + } + + const existingContact = await db.contactUs.findUnique({ + where: { + id: id + } + }); + + if (!existingContact) { + throw new APIError(404, "Contact not found"); + } + + const updatedContact = await db.contactUs.update({ + where: { + id: id + }, + data: { + status: status.toUpperCase() as ContactStatus + } + }); + + res.status(200).json({ + status: "success", + message: "Contact status updated successfully", + data: updatedContact + }); +}); + export default { getAllContactForms, + updateContactStatus, }; diff --git a/src/middlewares/auth.middleware.ts b/src/middlewares/auth.middleware.ts index 75f2406..90ca24b 100644 --- a/src/middlewares/auth.middleware.ts +++ b/src/middlewares/auth.middleware.ts @@ -9,6 +9,11 @@ export const authenticate = ( res: Response, next: NextFunction, ) => { + // Skip authentication for preflight OPTIONS requests + if (req.method === 'OPTIONS') { + return next(); + } + const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith("Bearer ")) { diff --git a/src/routes/v1/admin/routes.ts b/src/routes/v1/admin/routes.ts index da6bf8c..d87683b 100644 --- a/src/routes/v1/admin/routes.ts +++ b/src/routes/v1/admin/routes.ts @@ -7,6 +7,7 @@ const router = Router() router.use(authenticate) router.get("/contact/responses", v1Controllers.adminControllers.contactForm.contactFormsController.getAllContactForms) +router.put("/contact/responses/:id", v1Controllers.adminControllers.contactForm.contactFormsController.updateContactStatus) router.get("/waitlist/responses", v1Controllers.adminControllers.waitlist.waitlistController.getResponses) router.get("/waitlist/responses/status/:status", v1Controllers.adminControllers.waitlist.waitlistController.fetchResponsesByStatus) From 81ec4645828e969894c2f9f0eb4470d89be96939 Mon Sep 17 00:00:00 2001 From: Samarth Date: Fri, 7 Nov 2025 16:27:08 +0530 Subject: [PATCH 2/4] Update src/middlewares/auth.middleware.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/middlewares/auth.middleware.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/middlewares/auth.middleware.ts b/src/middlewares/auth.middleware.ts index 90ca24b..75f2406 100644 --- a/src/middlewares/auth.middleware.ts +++ b/src/middlewares/auth.middleware.ts @@ -9,11 +9,6 @@ export const authenticate = ( res: Response, next: NextFunction, ) => { - // Skip authentication for preflight OPTIONS requests - if (req.method === 'OPTIONS') { - return next(); - } - const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith("Bearer ")) { From 26fe3823f11c7c102bca7ae689ffbfcef6af96af Mon Sep 17 00:00:00 2001 From: Samarth Date: Fri, 7 Nov 2025 16:27:28 +0530 Subject: [PATCH 3/4] Update src/config/database.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/config/database.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/config/database.ts b/src/config/database.ts index 2ea2501..244a59e 100644 --- a/src/config/database.ts +++ b/src/config/database.ts @@ -9,11 +9,12 @@ interface CustomNodeJsGlobal extends Global { declare const global: CustomNodeJsGlobal; // Create PrismaClient with logging enabled to help diagnose connection issues -export const db = - global.prisma || - new PrismaClient({ +if (!global.prisma) { + global.prisma = new PrismaClient({ log: ["info", "warn", "error"], }); +} +export const db = global.prisma; // Track whether Prisma has successfully connected so other parts of the app // can report DB health without attempting queries that would fail. From 7e54e0a5aa871f29005a06cb0c81ba4831b8aa63 Mon Sep 17 00:00:00 2001 From: Samarth Date: Fri, 7 Nov 2025 16:27:55 +0530 Subject: [PATCH 4/4] Update src/app.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/app.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app.ts b/src/app.ts index cc141fb..7439819 100644 --- a/src/app.ts +++ b/src/app.ts @@ -15,11 +15,11 @@ const corsOptions = { // Allow localhost for development (only exact localhost origins) const localhostRegex = /^https?:\/\/localhost(:\d+)?$/; - if (origin && localhostRegex.test(origin)) return callback(null, true); + if (localhostRegex.test(origin)) return callback(null, true); // Allow ngrok tunnels (only valid ngrok.io subdomains) const ngrokRegex = /^https?:\/\/([a-z0-9-]+)\.ngrok\.io$/; - if (origin && ngrokRegex.test(origin)) return callback(null, true); + if (ngrokRegex.test(origin)) return callback(null, true); // Allow specific domains if needed const allowedOrigins = [