diff --git a/backend/tests/uploadPhoto.test.js b/backend/tests/uploadPhoto.test.js index 4936b5df..40fb9f55 100644 --- a/backend/tests/uploadPhoto.test.js +++ b/backend/tests/uploadPhoto.test.js @@ -166,9 +166,16 @@ describe("POST /api/users/upload-photo", () => { expect(res.status).toBe(200); expect(res.body.success).toBe(true); + + // Filename/path must contain the authenticated user's ID + expect(res.body.fileUrl).toContain(TEST_USER_ID); + const pathSegments = new URL(res.body.fileUrl).pathname.split("/").filter(Boolean); + expect(pathSegments).toContain(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 () => { diff --git a/docs/api.md b/docs/api.md index 00c71030..8e1c6feb 100644 --- a/docs/api.md +++ b/docs/api.md @@ -131,6 +131,47 @@ 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. + +## Upload Routes (`/api/upload` & `/api/users/upload-photo`) + +### `POST /api/upload` + +Uploads generic project resources with magic byte validation. + +**Auth**: Requires a valid Supabase JWT token in `Authorization: Bearer ` or `access_token` cookie. +**Content-Type**: `multipart/form-data` +**Form Fields**: +- `file` (File, required): The file to upload. +- `folder` (string, required): Permitted values are `avatars` or `resources`. + +**Response** (`200 OK`): +```json +{ + "success": true, + "data": { + "url": "https:///storage/v1/object/public/resources/user-id/filename.pdf" + } +} +``` + +### `POST /api/users/upload-photo` + +Uploads user profile photos (avatars) with magic byte validation. + +**Auth**: Requires a valid Supabase JWT token in `Authorization: Bearer ` or `access_token` cookie. +**Content-Type**: `multipart/form-data` +**Form Fields**: +- `profilePhoto` (File, required): The profile image file (max size: 2 MiB). + +**Response** (`200 OK`): +```json +{ + "success": true, + "fileUrl": "https:///storage/v1/object/public/avatars/user-id/filename.png" +} +``` + + ## 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. @@ -164,3 +205,4 @@ Profile-photo upload into the `profiles` bucket (2MB 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) +