Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ddd4d43
migrated oss sheets to md
huamanraj Nov 28, 2025
b51b929
fix: dynamic page for displaying sheet module content with a dedicate…
huamanraj Nov 28, 2025
2005156
perf: add header caching for sheet
apsinghdev Nov 29, 2025
01df440
fix: login page font fixed
huamanraj Nov 27, 2025
c13eb75
feat: fixed stats count size on small screens
mizurex Nov 27, 2025
3ad4701
ui: fix number size of stats
apsinghdev Nov 29, 2025
5aecef1
enhance root layout metadata
mizurex Nov 26, 2025
6c5d52c
bug-fix:navbar hides properly
praveenzsp Nov 23, 2025
30ed9b3
added back navbar to layout file
praveenzsp Nov 23, 2025
286f0b1
lint fix
praveenzsp Nov 23, 2025
49f740e
Fix typo in blog title ("shouln't" → "shouldn't")
SGNayak12 Nov 20, 2025
c6d38d2
fix: typos in blog titles
Lucifer-0612 Nov 22, 2025
15d2459
fix: normalize blog titles to lowercase
Lucifer-0612 Nov 24, 2025
560ced8
SideBar enhancement for Dashboard
Nov 18, 2025
da4989a
chore: extend offer
apsinghdev Nov 30, 2025
4643a68
feat: OSS programs added with data
huamanraj Nov 25, 2025
f4067d6
feat: update styles and improve accessibility for OSS program components
huamanraj Nov 25, 2025
a085764
fix: fix jsdom esmodule requirement err
apsinghdev Nov 29, 2025
247ceae
fix: ui repsnsiveness and design
huamanraj Nov 29, 2025
0b96b91
fix(ui): fix right side corners of oss programs card
apsinghdev Dec 1, 2025
133debd
fix(ui): fix right side corners of oss programs card
apsinghdev Dec 1, 2025
7479594
Merge branch 'main' of https://github.com/apsinghdev/opensox
huamanraj Dec 1, 2025
333d2b8
feat: add sponsor page
huamanraj Dec 3, 2025
d7c85f9
feat: Add sponsor program with dedicated pages, API, and payment inte…
huamanraj Dec 4, 2025
654d372
fix: removed auth required for sponsor
huamanraj Dec 5, 2025
3211bfd
style: update styling for sponsor components and improve layout
huamanraj Dec 5, 2025
ca0eca9
fix: update sponsor components for improved layout and responsiveness
huamanraj Dec 6, 2025
80435b7
fix: added multer for cloudnary
huamanraj Dec 6, 2025
caa83ee
fix: update file handling in sponsor upload to improve TypeScript com…
huamanraj Dec 6, 2025
da6089b
fix: enhance sponsor payment validation and subscription handling in …
huamanraj Dec 6, 2025
c0f297d
fix: add environment variable validation and improve payment handling…
huamanraj Dec 6, 2025
e89c77c
fix: enhance image upload validation and update multer dependency in …
huamanraj Dec 6, 2025
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
5 changes: 3 additions & 2 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"razorpay": "^2.9.6",
"superjson": "^2.2.5",
"zeptomail": "^6.2.1",
"zod": "^4.1.9"
"zod": "^4.1.9",
"cloudinary": "^2.0.0"
}
}
}
22 changes: 20 additions & 2 deletions apps/api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ model User {
accounts Account[]
payments Payment[]
subscriptions Subscription[]
sponsors Sponsor[]
}

model Account {
Expand All @@ -64,7 +65,7 @@ model Account {

model Payment {
id String @id @default(cuid())
userId String
userId String?
subscriptionId String?
razorpayPaymentId String @unique
razorpayOrderId String
Expand All @@ -74,7 +75,7 @@ model Payment {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
subscription Subscription? @relation(fields: [subscriptionId], references: [id])
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model Subscription {
Expand Down Expand Up @@ -102,3 +103,20 @@ model Plan {
updatedAt DateTime @updatedAt
subscriptions Subscription[]
}

model Sponsor {
id String @id @default(cuid())
userId String?
company_name String
description String
website String
image_url String
razorpay_payment_id String?
razorpay_sub_id String?
plan_status String // active, cancelled, pending_payment, pending_submission, failed
contact_name String? // customer name from razorpay
contact_email String? // customer email from razorpay
contact_phone String? // customer phone from razorpay
created_at DateTime @default(now())
user User? @relation(fields: [userId], references: [id])
}
187 changes: 87 additions & 100 deletions apps/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,34 @@ import crypto from "crypto";
import { paymentService } from "./services/payment.service.js";
import { verifyToken } from "./utils/auth.js";
import { SUBSCRIPTION_STATUS } from "./constants/subscription.js";
import multer from "multer";
import { v2 as cloudinary } from "cloudinary";
import { handleRazorpayWebhook } from "./webhooks.js";

dotenv.config();

// validate required environment variables early
const requiredEnv = [
"CLOUDINARY_CLOUD_NAME",
"CLOUDINARY_API_KEY",
"CLOUDINARY_API_SECRET",
"RAZORPAY_WEBHOOK_SECRET",
] as const;

for (const key of requiredEnv) {
if (!process.env[key] || process.env[key]!.trim().length === 0) {
console.error(`missing required env var: ${key}`);
process.exit(1);
}
}

// configure cloudinary (kept local to this route)
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME!,
api_key: process.env.CLOUDINARY_API_KEY!,
api_secret: process.env.CLOUDINARY_API_SECRET!,
});

const app = express();
const PORT = process.env.PORT || 4000;
const CORS_ORIGINS = process.env.CORS_ORIGINS
Expand Down Expand Up @@ -62,8 +87,9 @@ const apiLimiter = rateLimit({

// Request size limits (except for webhook - needs raw body)
app.use("/webhook/razorpay", express.raw({ type: "application/json" }));
app.use(express.json({ limit: "10kb" }));
app.use(express.urlencoded({ limit: "10kb", extended: true }));
// Reduce global JSON/urlencoded limits to prevent DoS
app.use(express.json({ limit: "5mb" }));
app.use(express.urlencoded({ limit: "5mb", extended: true }));

// CORS configuration
const corsOptions: CorsOptionsType = {
Expand Down Expand Up @@ -98,6 +124,63 @@ app.get("/test", apiLimiter, (req: Request, res: Response) => {
res.status(200).json({ status: "ok", message: "Test endpoint is working" });
});

// Secure multipart upload setup with strict validation
const upload = multer({
storage: multer.memoryStorage(), // avoid temp files; stream to Cloudinary
limits: {
fileSize: 10 * 1024 * 1024, // 10MB per-file limit for this endpoint
files: 1,
},
fileFilter: (_req, file, cb) => {
const allowed = ["image/png", "image/jpeg", "image/jpg", "image/webp"];
if (!allowed.includes(file.mimetype)) {
return cb(new Error("Invalid file type"));
}
cb(null, true);
},
});

// Dedicated upload endpoint that only accepts multipart/form-data
app.post(
"/upload/sponsor-image",
apiLimiter,
(req, res, next) => {
if (!req.is("multipart/form-data")) {
return res.status(415).json({ error: "Unsupported Media Type. Use multipart/form-data." });
}
next();
},
upload.single("file"),
async (req: Request, res: Response) => {
try {
if (!req.file) {
return res.status(400).json({ error: "No file provided" });
}

const file = req.file; // narrow for TypeScript across closures

// Stream upload to Cloudinary
const folder = "opensox/sponsors";
const result = await new Promise<any>((resolve, reject) => {
const stream = cloudinary.uploader.upload_stream({ folder }, (error, uploadResult) => {
if (error) return reject(error);
resolve(uploadResult);
});
stream.end(file.buffer);
});

return res.status(200).json({
url: result.secure_url,
bytes: file.size,
mimetype: file.mimetype,
});
} catch (err: any) {
const isLimit = err?.message?.toLowerCase()?.includes("file too large");
return res.status(isLimit ? 413 : 400).json({ error: err.message || "Upload failed" });
}
}
);

// Slack Community Invite Endpoint (Protected)
app.get("/join-community", apiLimiter, async (req: Request, res: Response) => {
try {
Expand Down Expand Up @@ -153,104 +236,8 @@ app.get("/join-community", apiLimiter, async (req: Request, res: Response) => {
}
});

// Razorpay Webhook Handler (Backup Flow)
app.post("/webhook/razorpay", async (req: Request, res: Response) => {
try {
const webhookSecret = process.env.RAZORPAY_WEBHOOK_SECRET;
if (!webhookSecret) {
console.error("RAZORPAY_WEBHOOK_SECRET not configured");
return res.status(500).json({ error: "Webhook not configured" });
}

// Get signature from headers
const signature = req.headers["x-razorpay-signature"] as string;
if (!signature) {
return res.status(400).json({ error: "Missing signature" });
}

// Verify webhook signature
const body = req.body.toString();
const expectedSignature = crypto
.createHmac("sha256", webhookSecret)
.update(body)
.digest("hex");

const isValidSignature = crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);

if (!isValidSignature) {
console.error("Invalid webhook signature");
return res.status(400).json({ error: "Invalid signature" });
}

// Parse the event
const event = JSON.parse(body);
const eventType = event.event;

// Handle payment.captured event
if (eventType === "payment.captured") {
const payment = event.payload.payment.entity;

// Extract payment details
const razorpayPaymentId = payment.id;
const razorpayOrderId = payment.order_id;
const amount = payment.amount;
const currency = payment.currency;

// Get user ID from order notes (should be stored when creating order)
const notes = payment.notes || {};
const userId = notes.user_id;

if (!userId) {
console.error("User ID not found in payment notes");
return res.status(400).json({ error: "User ID not found" });
}

// Get plan ID from notes
const planId = notes.plan_id;
if (!planId) {
console.error("Plan ID not found in payment notes");
return res.status(400).json({ error: "Plan ID not found" });
}

try {
// Create payment record (with idempotency check)
const paymentRecord = await paymentService.createPaymentRecord(userId, {
razorpayPaymentId,
razorpayOrderId,
amount,
currency,
});

// Create subscription (with idempotency check)
await paymentService.createSubscription(
userId,
planId,
paymentRecord.id
);

console.log(
`✅ Webhook: Payment ${razorpayPaymentId} processed successfully`
);
return res.status(200).json({ status: "ok" });
} catch (error: any) {
console.error("Webhook payment processing error:", error);
// Return 200 to prevent Razorpay retries for application errors
return res
.status(200)
.json({ status: "ok", note: "Already processed" });
}
}

// Acknowledge other events
return res.status(200).json({ status: "ok" });
} catch (error: any) {
console.error("Webhook error:", error);
return res.status(500).json({ error: "Internal server error" });
}
});
// Razorpay Webhook Handler
app.post("/webhook/razorpay", handleRazorpayWebhook);

// Connect to database
prismaModule.connectDB();
Expand Down
3 changes: 3 additions & 0 deletions apps/api/src/routers/_app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ const testRouter = router({
}),
});

import { sponsorRouter } from "./sponsor.js";

export const appRouter = router({
hello: testRouter,
query: queryRouter,
user: userRouter,
project: projectRouter,
auth: authRouter,
payment: paymentRouter,
sponsor: sponsorRouter,
});

export type AppRouter = typeof appRouter;
Loading