Skip to content
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
38 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
968c15b
fix: implement subscription handling and signature verification in sp…
huamanraj Dec 8, 2025
40787d1
fix: subscription and sponsor submission flow error handling
huamanraj Dec 8, 2025
06abc02
Merge branch 'main' into feat/sponsor
huamanraj Dec 8, 2025
2c272b8
fix: trpc types fix
huamanraj Dec 8, 2025
f584ca2
fix: ts errors
huamanraj Dec 8, 2025
7eea4a8
fix: mobile repsonsivness
huamanraj Dec 9, 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
12 changes: 12 additions & 0 deletions apps/api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,15 @@ model Plan {
updatedAt DateTime @updatedAt
subscriptions Subscription[]
}

model Sponsor {
id String @id @default(cuid())
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
created_at DateTime @default(now())
}
100 changes: 3 additions & 97 deletions apps/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,104 +153,10 @@ 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;
import { handleRazorpayWebhook } from "./webhooks.js";

// 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;
151 changes: 151 additions & 0 deletions apps/api/src/routers/sponsor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { router, publicProcedure, protectedProcedure } from "../trpc.js";
import { z } from "zod";
import prismaModule from "../prisma.js";
import { paymentService } from "../services/payment.service.js";

const { prisma } = prismaModule;

export const sponsorRouter = router({
// Create a subscription for sponsorship
createSubscription: protectedProcedure
.input(
z.object({
planId: z.string(),
})
)
.mutation(async ({ ctx, input }: { ctx: any, input: any }) => {
const user = ctx.user;

// Create Razorpay order
// Note: In a real scenario, we might want to fetch the plan price from DB
// For now, we'll assume a fixed price or fetch from plan
const plan = await prisma.plan.findUnique({
where: { id: input.planId },
});

if (!plan) {
throw new Error("Plan not found");
}

const order = await paymentService.createOrder({
amount: plan.price,
currency: plan.currency,
receipt: `sponsor_${user.id}_${Date.now()}`,
notes: {
user_id: user.id,
plan_id: input.planId,
type: "sponsor",
},
});

if ("error" in order) {
throw new Error(order.error.description);
}

return {
orderId: order.id,
amount: order.amount,
currency: order.currency,
key: process.env.RAZORPAY_KEY_ID,
};
}),

// Submit sponsor assets
// Submit sponsor assets
submitAssets: protectedProcedure
.input(
z.object({
companyName: z.string(),
description: z.string(),
website: z.string().url(),
imageUrl: z.string().url(),
razorpayPaymentId: z.string(),
})
)
.mutation(async ({ ctx, input }: { ctx: any, input: any }) => {
// Verify payment exists and is successful
const payment = await prisma.payment.findUnique({
where: { razorpayPaymentId: input.razorpayPaymentId },
});

if (!payment || payment.status !== "captured") {
throw new Error("Valid payment not found");
}

// Check if this payment belongs to the user
if (payment.userId !== ctx.user.id) {
throw new Error("Unauthorized");
}

// Upsert sponsor record
const existingSponsor = await prisma.sponsor.findFirst({
where: { razorpay_payment_id: input.razorpayPaymentId },
});

if (existingSponsor) {
return await prisma.sponsor.update({
where: { id: existingSponsor.id },
data: {
company_name: input.companyName,
description: input.description,
website: input.website,
image_url: input.imageUrl,
plan_status: "active",
},
});
} else {
return await prisma.sponsor.create({
data: {
company_name: input.companyName,
description: input.description,
website: input.website,
image_url: input.imageUrl,
razorpay_payment_id: input.razorpayPaymentId,
plan_status: "active",
},
});
}
}),

// Get pending sponsorships for the current user
getPendingSponsorship: protectedProcedure.query(async ({ ctx }: { ctx: any }) => {
const userPayments = await prisma.payment.findMany({
where: {
userId: ctx.user.id,
status: "captured",
},
orderBy: { createdAt: "desc" },
take: 5,
});

for (const payment of userPayments) {
const sponsor = await prisma.sponsor.findFirst({
where: { razorpay_payment_id: payment.razorpayPaymentId },
});

if (!sponsor || sponsor.plan_status === "pending_submission") {
// Check if this payment is likely for sponsorship (e.g. has subscriptionId)
if (payment.subscriptionId) {
return {
paymentId: payment.razorpayPaymentId,
amount: payment.amount,
date: payment.createdAt,
};
}
}
}
return null;
}),

// Get active sponsors
getActiveSponsors: publicProcedure.query(async () => {
return await prisma.sponsor.findMany({
where: {
plan_status: "active",
},
orderBy: {
created_at: "desc",
},
});
}),
});
Loading