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..7439819 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 (localhostRegex.test(origin)) return callback(null, true); + + // Allow ngrok tunnels (only valid ngrok.io subdomains) + const ngrokRegex = /^https?:\/\/([a-z0-9-]+)\.ngrok\.io$/; + if (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..244a59e 100644 --- a/src/config/database.ts +++ b/src/config/database.ts @@ -1,18 +1,55 @@ 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 +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. +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/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)