Skip to content

Commit 80435b7

Browse files
committed
fix: added multer for cloudnary
1 parent ca0eca9 commit 80435b7

File tree

7 files changed

+217
-53
lines changed

7 files changed

+217
-53
lines changed

apps/api/src/index.ts

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,19 @@ import crypto from "crypto";
1414
import { paymentService } from "./services/payment.service.js";
1515
import { verifyToken } from "./utils/auth.js";
1616
import { SUBSCRIPTION_STATUS } from "./constants/subscription.js";
17+
import multer from "multer";
18+
import { v2 as cloudinary } from "cloudinary";
19+
import { handleRazorpayWebhook } from "./webhooks.js";
1720

1821
dotenv.config();
1922

23+
// Configure Cloudinary (kept local to this route)
24+
cloudinary.config({
25+
cloud_name: process.env.CLOUDINARY_CLOUD_NAME || "",
26+
api_key: process.env.CLOUDINARY_API_KEY || "",
27+
api_secret: process.env.CLOUDINARY_API_SECRET || "",
28+
});
29+
2030
const app = express();
2131
const PORT = process.env.PORT || 4000;
2232
const CORS_ORIGINS = process.env.CORS_ORIGINS
@@ -62,8 +72,9 @@ const apiLimiter = rateLimit({
6272

6373
// Request size limits (except for webhook - needs raw body)
6474
app.use("/webhook/razorpay", express.raw({ type: "application/json" }));
65-
app.use(express.json({ limit: "50mb" }));
66-
app.use(express.urlencoded({ limit: "50mb", extended: true }));
75+
// Reduce global JSON/urlencoded limits to prevent DoS
76+
app.use(express.json({ limit: "5mb" }));
77+
app.use(express.urlencoded({ limit: "5mb", extended: true }));
6778

6879
// CORS configuration
6980
const corsOptions: CorsOptionsType = {
@@ -98,6 +109,61 @@ app.get("/test", apiLimiter, (req: Request, res: Response) => {
98109
res.status(200).json({ status: "ok", message: "Test endpoint is working" });
99110
});
100111

112+
// Secure multipart upload setup with strict validation
113+
const upload = multer({
114+
storage: multer.memoryStorage(), // avoid temp files; stream to Cloudinary
115+
limits: {
116+
fileSize: 10 * 1024 * 1024, // 10MB per-file limit for this endpoint
117+
files: 1,
118+
},
119+
fileFilter: (_req, file, cb) => {
120+
const allowed = ["image/png", "image/jpeg", "image/jpg", "image/webp"];
121+
if (!allowed.includes(file.mimetype)) {
122+
return cb(new Error("Invalid file type"));
123+
}
124+
cb(null, true);
125+
},
126+
});
127+
128+
// Dedicated upload endpoint that only accepts multipart/form-data
129+
app.post(
130+
"/upload/sponsor-image",
131+
apiLimiter,
132+
(req, res, next) => {
133+
if (!req.is("multipart/form-data")) {
134+
return res.status(415).json({ error: "Unsupported Media Type. Use multipart/form-data." });
135+
}
136+
next();
137+
},
138+
upload.single("file"),
139+
async (req: Request, res: Response) => {
140+
try {
141+
if (!req.file) {
142+
return res.status(400).json({ error: "No file provided" });
143+
}
144+
145+
// Stream upload to Cloudinary
146+
const folder = "opensox/sponsors";
147+
const result = await new Promise<any>((resolve, reject) => {
148+
const stream = cloudinary.uploader.upload_stream({ folder }, (error, uploadResult) => {
149+
if (error) return reject(error);
150+
resolve(uploadResult);
151+
});
152+
stream.end(req.file.buffer);
153+
});
154+
155+
return res.status(200).json({
156+
url: result.secure_url,
157+
bytes: req.file.size,
158+
mimetype: req.file.mimetype,
159+
});
160+
} catch (err: any) {
161+
const isLimit = err?.message?.toLowerCase()?.includes("file too large");
162+
return res.status(isLimit ? 413 : 400).json({ error: err.message || "Upload failed" });
163+
}
164+
}
165+
);
166+
101167
// Slack Community Invite Endpoint (Protected)
102168
app.get("/join-community", apiLimiter, async (req: Request, res: Response) => {
103169
try {
@@ -153,8 +219,6 @@ app.get("/join-community", apiLimiter, async (req: Request, res: Response) => {
153219
}
154220
});
155221

156-
import { handleRazorpayWebhook } from "./webhooks.js";
157-
158222
// Razorpay Webhook Handler
159223
app.post("/webhook/razorpay", handleRazorpayWebhook);
160224

apps/api/src/routers/sponsor.ts

Lines changed: 47 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -89,45 +89,61 @@ export const sponsorRouter = router({
8989
});
9090
}
9191

92-
// check if payment already exists
93-
const existingPayment = await prisma.payment.findUnique({
92+
// CRITICAL: validate order attributes to prevent replay/misuse
93+
const order = await rz_instance.orders.fetch(input.razorpay_order_id);
94+
if (
95+
!order ||
96+
order.amount !== SPONSOR_MONTHLY_AMOUNT ||
97+
order.currency !== SPONSOR_CURRENCY ||
98+
order.notes?.type !== "sponsor"
99+
) {
100+
throw new TRPCError({
101+
code: "BAD_REQUEST",
102+
message: "invalid order details",
103+
});
104+
}
105+
106+
// Upsert payment to avoid race conditions / duplicates
107+
await prisma.payment.upsert({
94108
where: { razorpayPaymentId: input.razorpay_payment_id },
109+
update: {
110+
razorpayOrderId: input.razorpay_order_id,
111+
amount: SPONSOR_MONTHLY_AMOUNT,
112+
currency: SPONSOR_CURRENCY,
113+
status: "captured",
114+
},
115+
create: {
116+
razorpayPaymentId: input.razorpay_payment_id,
117+
razorpayOrderId: input.razorpay_order_id,
118+
amount: SPONSOR_MONTHLY_AMOUNT,
119+
currency: SPONSOR_CURRENCY,
120+
status: "captured",
121+
},
95122
});
96123

97-
if (!existingPayment) {
98-
// create payment record without user (userId is optional)
99-
await prisma.payment.create({
100-
data: {
101-
razorpayPaymentId: input.razorpay_payment_id,
102-
razorpayOrderId: input.razorpay_order_id,
103-
amount: SPONSOR_MONTHLY_AMOUNT,
104-
currency: SPONSOR_CURRENCY,
105-
status: "captured",
106-
},
107-
});
108-
}
124+
// Fetch payment details with proper typing
125+
type RazorpayPaymentDetails = {
126+
contact?: string | number;
127+
email?: string;
128+
notes?: { name?: string } | null;
129+
};
130+
131+
const paymentDetails = (await rz_instance.payments.fetch(
132+
input.razorpay_payment_id
133+
)) as RazorpayPaymentDetails;
109134

110-
// fetch payment details from razorpay to get customer contact information
111135
let contactName: string | null = null;
112136
let contactEmail: string | null = null;
113137
let contactPhone: string | null = null;
114138

115-
try {
116-
const paymentDetails: any = await rz_instance.payments.fetch(input.razorpay_payment_id);
117-
// extract customer details from payment
118-
if (paymentDetails.contact) {
119-
contactPhone = String(paymentDetails.contact);
120-
}
121-
if (paymentDetails.email) {
122-
contactEmail = String(paymentDetails.email);
123-
}
124-
// note: name might not be directly in payment object, check notes or order
125-
if (paymentDetails.notes && paymentDetails.notes.name) {
126-
contactName = String(paymentDetails.notes.name);
127-
}
128-
} catch (error) {
129-
console.error("failed to fetch payment details from razorpay:", error);
130-
// continue without contact details if fetch fails
139+
if (paymentDetails.contact != null) {
140+
contactPhone = String(paymentDetails.contact);
141+
}
142+
if (paymentDetails.email) {
143+
contactEmail = String(paymentDetails.email);
144+
}
145+
if (paymentDetails.notes?.name) {
146+
contactName = String(paymentDetails.notes.name);
131147
}
132148

133149
// create or update sponsor record with pending_submission status

apps/api/src/webhooks.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,5 @@ async function handleSubscriptionStatusChange(eventType: string, payload: any) {
217217
},
218218
});
219219
}
220+
221+
// No changes required for webhook handlers in this change set.

apps/web/src/app/(main)/sponsor/submit/page.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,13 @@ const SponsorSubmitPage = () => {
2929
</p>
3030
<button
3131
onClick={() => router.push("/sponsor")}
32-
className="bg-[#4dd0a4] text-black font-bold py-2 px-6 rounded-lg"
32+
className="
33+
bg-primary text-primary-foreground
34+
font-bold py-2 px-6 rounded-lg
35+
hover:bg-primary/90
36+
focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background
37+
transition-colors
38+
"
3339
>
3440
Go to Sponsor Page
3541
</button>

apps/web/src/components/landing-sections/SponsorSection.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const SponsorSection = () => {
1616
const hasSponsors = sponsors && sponsors.length > 0;
1717

1818
return (
19-
<section className="w-full py-16 lg:py-24 px-4 lg:px-[60px] border-b border-[#252525] flex flex-col items-center justify-center gap-10">
19+
<section className="w-full py-16 lg:py-24 px-4 lg:px-[60px] border-b border-border flex flex-col items-center justify-center gap-10">
2020
<div className="w-full max-w-7xl mx-auto space-y-10">
2121
<div className="text-center space-y-4">
2222
<h2 className="text-3xl md:text-5xl font-medium tracking-tighter text-white">
@@ -36,7 +36,7 @@ const SponsorSection = () => {
3636
{Array.from({ length: Math.max(0, 3 - (sponsors?.length || 0)) }).map(
3737
(_, index) => (
3838
<Link key={`placeholder-${index}`} href="/sponsor" className="group block h-full">
39-
<div className="aspect-[16/10] w-full h-full rounded-2xl border border-dashed border-[#252525] bg-neutral-900/20 hover:bg-neutral-900/40 hover:border-neutral-700 transition-all duration-300 flex flex-col items-center justify-center gap-3 p-6">
39+
<div className="aspect-[16/10] w-full h-full rounded-2xl border border-dashed border-border bg-neutral-900/20 hover:bg-neutral-900/40 hover:border-border.light transition-all duration-300 flex flex-col items-center justify-center gap-3 p-6">
4040
<div className="w-16 h-16 rounded-full bg-neutral-800 flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
4141
<span className="text-2xl text-neutral-500">+</span>
4242
</div>

apps/web/src/components/sponsor/SponsorForm.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export const SponsorForm: React.FC<SponsorFormProps> = ({
3636

3737
const form = useForm<z.infer<typeof formSchema>>({
3838
resolver: zodResolver(formSchema),
39+
mode: "onChange",
3940
defaultValues: {
4041
companyName: "",
4142
description: "",
@@ -88,6 +89,17 @@ export const SponsorForm: React.FC<SponsorFormProps> = ({
8889
});
8990
};
9091

92+
// watch form values to check if all fields are completed
93+
const watchedValues = form.watch();
94+
const isFormValid =
95+
watchedValues.companyName?.trim().length >= 2 &&
96+
watchedValues.description?.trim().length >= 10 &&
97+
watchedValues.website?.trim().length > 0 &&
98+
form.formState.isValid &&
99+
imageUrl &&
100+
!uploading;
101+
const isSubmitting = submitAssetsMutation.isPending;
102+
91103
return (
92104
<div className="max-w-4xl mx-auto">
93105
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
@@ -293,10 +305,10 @@ export const SponsorForm: React.FC<SponsorFormProps> = ({
293305
<div className="pt-4">
294306
<button
295307
type="submit"
296-
disabled={submitAssetsMutation.isPending || uploading || !imageUrl}
297-
className="w-full sm:w-auto sm:min-w-[200px] bg-[#4dd0a4] text-black font-semibold py-3.5 px-8 rounded-xl hover:bg-[#3db08a] transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 group"
308+
disabled={!isFormValid || isSubmitting}
309+
className="w-full sm:w-auto sm:min-w-[200px] bg-primary text-primary-foreground font-semibold py-3.5 px-8 rounded-lg hover:bg-primary/90 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 group"
298310
>
299-
{submitAssetsMutation.isPending ? (
311+
{isSubmitting ? (
300312
<>
301313
<Loader2 className="w-5 h-5 animate-spin" />
302314
<span>Submitting...</span>
@@ -320,9 +332,9 @@ export const SponsorForm: React.FC<SponsorFormProps> = ({
320332
</>
321333
)}
322334
</button>
323-
{!imageUrl && (
335+
{(!isFormValid || !imageUrl) && (
324336
<p className="text-xs text-neutral-500 mt-3">
325-
Please upload your company logo to continue
337+
Please complete all fields to submit
326338
</p>
327339
)}
328340
</div>

0 commit comments

Comments
 (0)