Skip to content

Profile Picture Uploads - #177

Merged
renatodellosso merged 10 commits into
mainfrom
profile-picture-uploads
Apr 4, 2026
Merged

renatodellosso merged 10 commits into
mainfrom
profile-picture-uploads

Conversation

@ayan-goel

Copy link
Copy Markdown
Collaborator

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 — accepts multipart/form-data with a file field
  • Auth: any authenticated user with an active org membership (relaxed from the admin/owner requirement on /api/media)
  • Validates: raster images only (SVG blocked), ≤ 10 MB
  • Stores the file via FileService, creates a media record in the active org's library, returns { url: "/images/<mediaId>" }

lib/app.ts / app/api/[[...route]]/route.ts

  • Register the new /profile router in both the live Next.js adapter and the unit-test client

components/EditProfileModal.tsx

  • Add pendingImageFileRef (useRef<File | null>) to track the raw File selected by the user alongside the existing base64 preview
  • On save: upload the file if one is pending (replace the // image upload tbd comment), or pass image: null if the user clicked "Remove photo"; omit image entirely when nothing changed
  • Clear the ref on discard / modal close

tests/e2e/profile.spec.ts

  • Two new Playwright tests under "Profile Picture Upload":
    • uploads a 1×1 JPEG and asserts the profile image is visible after save
    • removes an existing photo and asserts no image is shown after save
  • Uses createTestAdminAndSignIn (requires activeOrganizationId on the session for the upload endpoint)

Notes

  • Profile photos land in the active org's media library under a UUID filename; the user.image field is set to /images/<mediaId>, served by the existing serveImage route (requires org membership to view)
  • No image-cropping UI — deferred per spec

Checklist:

  • I have mentioned a ticket above
  • My changes meet the acceptance criteria of said ticket
  • I have self-reviewed my changes
  • I have requested a review from the appropriate team member(s)
  • I have fixed all merge conflicts
  • I have ensured CI checks pass

@netlify

netlify Bot commented Apr 4, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for servicestart ready!

Name Link
🔨 Latest commit cea9d7f
🔍 Latest deploy log https://app.netlify.com/projects/servicestart/deploys/69d16e5baa70a50008daa79c
😎 Deploy Preview https://deploy-preview-177--servicestart.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@greptile-apps

greptile-apps Bot commented Apr 4, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR wires up the end-to-end profile picture upload flow: a new POST /api/profile/picture Hono endpoint handles multipart upload, stores the file, creates a media record, and returns a URL; EditProfileModal tracks the pending File in a ref and uploads on save before calling authClient.updateUser. The E2E tests cover the upload and remove paths using an admin session.

The two-step client-side flow (upload → then update user) leaves a window where a transient failure in authClient.updateUser produces an orphaned file and media record without updating the user's profile image. Additionally, any error during save (including the new upload step) triggers the pre-existing catch behavior that closes the modal and discards all form edits, which is a poor UX for a recoverable failure.

Confidence Score: 4/5

Safe 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

Filename Overview
api/profile.ts New upload endpoint with solid validation; contains a non-null assertion on activeOrganizationId (safe via requireMembership) and a two-step file+DB write with no rollback if the DB write fails after the file is stored.
components/EditProfileModal.tsx Correctly tracks pending file via ref, handles the upload/remove/unchanged cases, and spreads image into updateUser only when changed; on any error the modal closes and all edits are lost (pre-existing pattern).
lib/app.ts Registers the new /profile route in the test client — straightforward one-liner change.
app/api/[[...route]]/route.ts Registers the new /profile route in the Next.js adapter — straightforward one-liner change.
tests/e2e/profile.spec.ts Two new E2E tests covering upload and remove flows using createTestAdminAndSignIn; coverage is thorough for the happy paths.

Sequence Diagram

sequenceDiagram
    participant U as User (Browser)
    participant FE as EditProfileModal
    participant API as POST /api/profile/picture
    participant FS as FileService
    participant MS as MediaService
    participant AC as authClient.updateUser

    U->>FE: Select file → click Save
    FE->>API: POST multipart/form-data {file}
    API->>API: requireMembership() / validate type+size
    API->>FS: upload(mediaInput, file)
    FS-->>API: ok
    API->>MS: create(mediaInput)
    MS-->>API: media {id}
    API-->>FE: 201 {url: "/images/<id>"}
    FE->>AC: updateUser({image: url, ...fields})
    AC-->>FE: ok
    FE-->>U: redirect /profile?saved=true

    note over API,MS: If MS.create fails after FS.upload,<br/>file is orphaned in storage
    note over FE,AC: If updateUser fails after upload,<br/>media record exists but user.image unchanged
Loading

Greploops — Automatically fix all review issues by running /greploops in Claude Code. It iterates: fix, push, re-review, repeat until 5/5 confidence.
Use the Greptile plugin for Claude Code to query reviews, search comments, and manage custom context directly from your terminal.

Reviews (2): Last reviewed commit: "Merge branch 'main' into profile-picture..." | Re-trigger Greptile

Comment thread api/profile.ts
Comment on lines +13 to +19
function buildStoredFileName(originalName: string) {
const extension = path.extname(originalName).toLowerCase();
return `${randomUUID()}${extension}`;
}

function buildTitle(originalName: string) {
return path.basename(originalName, path.extname(originalName));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Comment thread api/profile.ts
const fileName = buildStoredFileName(file.name ?? `pfp-${Date.now()}`);
const mediaInput = {
organizationId: activeOrganizationId,
title: buildTitle(file.name) ?? "Profile photo",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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:

Suggested change
title: buildTitle(file.name) ?? "Profile photo",
title: buildTitle(file.name) || "Profile photo",

@renatodellosso renatodellosso left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work!

Comment thread api/profile.ts Outdated
Comment on lines +36 to +45
const membership = await MembersService.findByUserAndOrganization(
session.user.id,
activeOrganizationId,
);
if (!membership) {
return c.json(
{ error: "Forbidden: Organization membership required" },
{ status: 403 },
);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use requireMembership from authUtils.ts.

@renatodellosso
renatodellosso merged commit bdf9a4c into main Apr 4, 2026
24 of 25 checks passed
@renatodellosso
renatodellosso deleted the profile-picture-uploads branch April 4, 2026 20:08

This branch was previously deployed

1 inactive deployment
Preview — cea9d7f0 Deployed Apr 4, 2026 by renatodellosso via drop-db #169
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Profile Picture Uploads

2 participants