Repository navigation
Profile Picture Uploads - #177
Conversation
✅ Deploy Preview for servicestart ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Greptile SummaryThis PR wires up the end-to-end profile picture upload flow: a new The two-step client-side flow (upload → then update user) leaves a window where a transient failure in Confidence Score: 4/5Safe to merge with awareness of the two-step client flow; the orphaned-media and modal-close-on-error issues are worth addressing but don't block the happy path. All remaining findings are P2 — no runtime crashes or data corruption on the success path. The orphaned media concern is a reliability/cleanup issue under a narrow failure window, and the modal-close-on-error is a UX regression worth fixing in a follow-up. Core upload, validation, auth, and E2E coverage are solid. components/EditProfileModal.tsx — the save error handling path and the two-step upload/updateUser flow. Important Files Changed
|
| function buildStoredFileName(originalName: string) { | ||
| const extension = path.extname(originalName).toLowerCase(); | ||
| return `${randomUUID()}${extension}`; | ||
| } | ||
|
|
||
| function buildTitle(originalName: string) { | ||
| return path.basename(originalName, path.extname(originalName)); |
There was a problem hiding this comment.
Duplicated helpers and constants from
api/media.ts
buildStoredFileName, buildTitle, MAX_IMAGE_SIZE_BYTES, and BLOCKED_IMAGE_TYPES are identical copies of the same declarations in api/media.ts. If the size limit or blocked-type list ever needs updating, both files must change in sync. Consider extracting these to a shared lib/mediaUtils.ts (or similar) and importing from both endpoints.
| const fileName = buildStoredFileName(file.name ?? `pfp-${Date.now()}`); | ||
| const mediaInput = { | ||
| organizationId: activeOrganizationId, | ||
| title: buildTitle(file.name) ?? "Profile photo", |
There was a problem hiding this comment.
Dead fallback —
buildTitle never returns null
path.basename(...) always returns a string, never null or undefined, so the ?? "Profile photo" branch is unreachable. If file.name is an empty string the title stored in the DB will be "", not the intended fallback. Explicitly handle the empty-string case:
| title: buildTitle(file.name) ?? "Profile photo", | |
| title: buildTitle(file.name) || "Profile photo", |
| const membership = await MembersService.findByUserAndOrganization( | ||
| session.user.id, | ||
| activeOrganizationId, | ||
| ); | ||
| if (!membership) { | ||
| return c.json( | ||
| { error: "Forbidden: Organization membership required" }, | ||
| { status: 403 }, | ||
| ); | ||
| } |
There was a problem hiding this comment.
Use requireMembership from authUtils.ts.
Closes #95
Profile picture uploads
Wires up the stubbed-out profile picture upload flow end-to-end.
What changed
api/profile.ts(new)POST /api/profile/picture— acceptsmultipart/form-datawith afilefield/api/media)FileService, creates amediarecord in the active org's library, returns{ url: "/images/<mediaId>" }lib/app.ts/app/api/[[...route]]/route.ts/profilerouter in both the live Next.js adapter and the unit-test clientcomponents/EditProfileModal.tsxpendingImageFileRef(useRef<File | null>) to track the rawFileselected by the user alongside the existing base64 preview// image upload tbdcomment), or passimage: nullif the user clicked "Remove photo"; omitimageentirely when nothing changedtests/e2e/profile.spec.ts"Profile Picture Upload":createTestAdminAndSignIn(requiresactiveOrganizationIdon the session for the upload endpoint)Notes
user.imagefield is set to/images/<mediaId>, served by the existingserveImageroute (requires org membership to view)Checklist: