Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,24 @@ 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 {
PENDING
VERIFIED
REJECTED
}

enum ContactStatus {
UNREAD
READ
REPLIED
}
36 changes: 35 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand Down
53 changes: 45 additions & 8 deletions src/config/database.ts
Original file line number Diff line number Diff line change
@@ -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));
4 changes: 3 additions & 1 deletion src/config/envVars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof EnvConfigSchema>;
Expand All @@ -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,
};

Expand All @@ -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;
52 changes: 51 additions & 1 deletion src/controllers/v1/admin/contactForm/contactForms.controller.ts
Original file line number Diff line number Diff line change
@@ -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,
};
1 change: 1 addition & 0 deletions src/routes/v1/admin/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down