From bb07ca068626b5c78fd92e7ed9e8dffea3c48ba6 Mon Sep 17 00:00:00 2001 From: Sumit Kumar Date: Thu, 6 Aug 2026 17:38:49 +0530 Subject: [PATCH 1/2] fix(app): remove duplicate Settings lazy import App.tsx declared Settings twice which broke tsc. Also type the useSessions realtime broadcast callbacks so the frontend typecheck passes again. Co-authored-by: Cursor --- src/App.tsx | 1 - src/hooks/useSessions.ts | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index e56ec217..52b17fad 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -47,7 +47,6 @@ const Profile = React.lazy(() => import("./pages/Profile")); const EditProfile = React.lazy(() => import("./pages/EditProfile")); const Settings = React.lazy(() => import("./pages/Settings")); const Notifications = React.lazy(() => import("./pages/Notifications")); -const Settings = React.lazy(() => import("./pages/Settings")); const Leaderboard = React.lazy(() => import("./pages/Leaderboard")); const Admin = React.lazy(() => import("./pages/Admin")); const ForgotPassword = React.lazy(() => import("./pages/ForgotPassword")); diff --git a/src/hooks/useSessions.ts b/src/hooks/useSessions.ts index f3286825..17dc2944 100644 --- a/src/hooks/useSessions.ts +++ b/src/hooks/useSessions.ts @@ -180,16 +180,16 @@ export function useSessions(user: any) { const state = roomChannel.presenceState(); setParticipantCount(Math.max(1, Object.keys(state).length)); }) - .on("broadcast", { event: "typing" }, ({ payload }) => { - if (payload.user === (user?.user_metadata?.full_name || "Someone")) return; + .on("broadcast", { event: "typing" }, ({ payload }: { payload: { user?: string } }) => { + if (!payload.user || payload.user === (user?.user_metadata?.full_name || "Someone")) return; setTypingUser(payload.user); clearTimeout(typingTimeoutRef.current); typingTimeoutRef.current = setTimeout(() => setTypingUser(null), 3000); }) - .on("broadcast", { event: "activity" }, ({ payload }) => { + .on("broadcast", { event: "activity" }, ({ payload }: { payload: any }) => { setActivities((prev) => [payload, ...prev]); }) - .subscribe(async (status) => { + .subscribe(async (status: string) => { if (status === "SUBSCRIBED" && isMounted) { await roomChannel.track({ online_at: new Date().toISOString() }); From c1d2d6f9f151c558527fd78f629b18863db14885 Mon Sep 17 00:00:00 2001 From: Sumit Kumar Date: Sat, 8 Aug 2026 03:20:03 +0530 Subject: [PATCH 2/2] fix(ci): align upload tests and docs with current upload API Update profile-photo assertions for Supabase storage paths and the 2MB limit, and document /api/upload plus /api/users/upload-photo so docs completeness checks pass. Co-authored-by: Cursor --- backend/tests/uploadPhoto.test.js | 11 +++++----- docs/api.md | 34 +++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/backend/tests/uploadPhoto.test.js b/backend/tests/uploadPhoto.test.js index 0fa23380..4936b5df 100644 --- a/backend/tests/uploadPhoto.test.js +++ b/backend/tests/uploadPhoto.test.js @@ -166,8 +166,9 @@ describe("POST /api/users/upload-photo", () => { expect(res.status).toBe(200); expect(res.body.success).toBe(true); - // Filename must contain the authenticated user's ID - expect(res.body.fileUrl).toMatch(new RegExp(`profile-${TEST_USER_ID}-`)); + // Supabase storage path is scoped to the authenticated user's ID + expect(res.body.fileUrl).toContain(`${TEST_USER_ID}/`); + expect(storageUploadMock).toHaveBeenCalled(); }); it("returns 200 when a valid JWT is supplied via HttpOnly cookie", async () => { @@ -221,9 +222,9 @@ describe("POST /api/users/upload-photo", () => { expect(res.body.error).toMatch(/no file/i); }); - it("returns 413 when the uploaded file exceeds the 5MB size limit", async () => { + it("returns 413 when the uploaded file exceeds the 2MB size limit", async () => { const token = makeToken(); - const oversized = Buffer.alloc(6 * 1024 * 1024, 0xff); // 6 MB of 0xFF bytes + const oversized = Buffer.alloc(3 * 1024 * 1024, 0xff); // 3 MB of 0xFF bytes const res = await request(app) .post("/api/users/upload-photo") .set("Authorization", `Bearer ${token}`) @@ -233,7 +234,7 @@ describe("POST /api/users/upload-photo", () => { }); expect(res.status).toBe(413); - expect(res.body.error).toMatch(/5mb/i); + expect(res.body.error).toMatch(/2mb/i); }); }); diff --git a/docs/api.md b/docs/api.md index d8cda768..00c71030 100644 --- a/docs/api.md +++ b/docs/api.md @@ -130,3 +130,37 @@ Sends a browser push notification to all subscribed devices for a given `user_id ``` **Security**: Standard users may only send push notifications to themselves (IDOR prevention). Webhook callers authenticated via `WEBHOOK_SECRET` may send to any user. + +## File Upload Routes + +Authenticated multipart uploads are written to Supabase Storage. Storage paths are generated on the server from the caller's user id — clients cannot choose arbitrary object keys. + +### `POST /api/upload` + +General-purpose upload for `avatars`, `profiles`, and `resources` buckets. + +**Auth**: valid Supabase JWT (`Authorization` header or `access_token` cookie) + +**Form fields**: +- `folder`: one of `avatars`, `profiles`, `resources` +- `file`: the file to upload + +**Validation**: +- MIME type must match the destination folder allow-list +- Magic byte / content-type verification rejects spoofed uploads +- Binary content and null bytes are rejected for text resource uploads + +### `POST /api/users/upload-photo` + +Profile-photo upload into the `profiles` bucket (2MB limit). + +**Auth**: valid Supabase JWT (`Authorization` header or `access_token` cookie) + +**Form fields**: +- `profilePhoto`: JPEG, PNG, WebP, or GIF image + +**Validation**: +- 2MB size limit +- Strict image MIME allow-list +- Magic byte verification that file content matches the declared image type +- Per-user rate limit (10 uploads per hour)