Skip to content

Feat: Add local profile avatar upload and UI integration (#279)#372

Open
Avnxxh wants to merge 2 commits intoAOSSIE-Org:mainfrom
Avnxxh:fix/Profile-Image-Upload
Open

Feat: Add local profile avatar upload and UI integration (#279)#372
Avnxxh wants to merge 2 commits intoAOSSIE-Org:mainfrom
Avnxxh:fix/Profile-Image-Upload

Conversation

@Avnxxh
Copy link
Copy Markdown

@Avnxxh Avnxxh commented Mar 24, 2026

Addressed Issues:

Fixes #279

Additional Notes:

This PR implements the **Profile Avatar Upload Feature (Local File Support) **, allowing users to upload custom profile images from their local device.


🚀 Features Implemented

🔧 Backend

  • Added new controller: upload_controller.go
  • Implemented UploadAvatar handler:
    • Accepts multipart file uploads
    • Validates file types (.jpg, .jpeg, .png)
    • Enforces max file size (5MB)
    • Stores files in ./uploads/avatars/
    • Returns publicly accessible avatar URL
  • Updated server configuration in main.go:
    • Enabled static file serving via /uploads
    • Registered protected route: /user/upload-avatar

🌐 Frontend Service

  • Added uploadAvatar(token, file) in profileService.ts
  • Uses FormData for file upload
  • Handles API response and returns avatar URL

🎨 UI Enhancements

  • Integrated file upload functionality in Profile.tsx
  • Added hidden file input for seamless UX
  • Enhanced avatar interaction:
    • On hover, shows:
      • Upload Custom Avatar
      • Avatar Customization (DiceBear)
  • Avatar updates instantly after upload
  • Changes persist after page refresh

🎯 Result

  • Users can upload custom profile images
  • Immediate UI update after upload
  • Persistent avatar across sessions
  • Maintains existing DiceBear functionality

🧪 How to Test

Backend:

  1. Send POST request to /user/upload-avatar
  2. Upload JPG/PNG file (<5MB)
  3. Verify file is saved in uploads/avatars/
  4. Confirm returned URL is accessible

Frontend:

  1. Login and navigate to Profile page
  2. Hover over avatar → click "Upload Custom Avatar"
  3. Select image file
  4. Verify:
    • Avatar updates instantly
    • Refresh page → avatar persists

AI Usage Disclosure:

  • This PR contains AI-generated code. I have read the AI Usage Policy and this PR complies with this policy. I have tested the code locally and I am responsible for it.

I have used the following AI models and tools:

  • ChatGPT (for guidance and implementation support)

Checklist

  • My PR addresses a single issue, fixes a single bug or makes a single improvement.
  • My code follows the project's code style and conventions
  • If applicable, I have made corresponding changes or additions to the documentation
  • If applicable, I have made corresponding changes or additions to tests
  • My changes generate no new warnings or errors
  • I have joined the Discord server and I will share a link to this PR with the project maintainers there
  • I have read the Contribution Guidelines
  • Once I submit my PR, CodeRabbit AI will automatically review it and I will address CodeRabbit's comments.
  • I have filled this PR template completely and carefully, and I understand that my PR may be closed without review otherwise.

Summary by CodeRabbit

  • New Features

    • Users can upload and update profile avatars from the Profile page with JPG/JPEG/PNG support and a 5MB limit.
    • Avatar uploads persist immediately and are served via a public uploads URL.
    • UI adds a two-action overlay (open editor + direct file upload) and a hidden file input for direct selection.
    • Client includes an authenticated upload action.
  • Improvements

    • Optimistic avatar update with rollback and clear error messaging on failure.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 24, 2026

📝 Walkthrough

Walkthrough

Adds local avatar upload: backend serves ./uploads statically, creates uploads/avatars at startup, and exposes POST /user/upload-avatar. A new controller accepts multipart JPG/JPEG/PNG uploads (<=5MB), saves them to ./uploads/avatars/, and returns an avatar URL. Frontend uploads files, updates state optimistically, and persists the profile.

Changes

Cohort / File(s) Summary
Server setup & routing
backend/cmd/server/main.go, backend/routes/profile.go
Creates uploads/avatars at startup (explicit permissions, fail-fast), serves ./uploads at /uploads, and registers protected POST /user/upload-avatar route.
Backend upload controller
backend/controllers/upload_controller.go
New UploadAvatar handler: enforces 5MB body limit, accepts avatar multipart field, validates .jpg/.jpeg/.png (case-insensitive), ensures ./uploads/avatars exists, generates UUID filename, saves file, and returns a relative avatarUrl.
Frontend profile service
frontend/src/services/profileService.ts
New exported uploadAvatar(token, file) that sends FormData to /user/upload-avatar with auth and returns parsed JSON (throws on non-OK).
Frontend profile UI
frontend/src/Pages/Profile.tsx
Adds hidden file input, fileInputRef, handleFileUpload enforcing 5MB limit and token presence; calls uploadAvatar, updates dashboard.profile.avatarUrl optimistically, persists via updateProfile, handles revert on failure, and adds a dedicated upload button in avatar overlay.

Sequence Diagram

sequenceDiagram
    participant User
    participant Frontend as Frontend UI<br/>(Profile.tsx)
    participant Service as Profile Service<br/>(profileService.ts)
    participant Backend as Backend<br/>(upload_controller.go)
    participant FileSystem as File System<br/>(./uploads/avatars)

    User->>Frontend: Click "Upload Avatar"
    Frontend->>Frontend: Open file input dialog
    User->>Frontend: Select JPG/PNG file
    Frontend->>Frontend: Validate file (size ≤ 5MB)

    rect rgba(0, 150, 136, 0.5)
    Frontend->>Service: uploadAvatar(token, file)
    Service->>Service: Attach file to FormData
    Service->>Backend: POST /user/upload-avatar (multipart/form-data)
    end

    rect rgba(63, 81, 181, 0.5)
    Backend->>Backend: Enforce size limit & validate extension
    Backend->>Backend: Generate UUID filename
    Backend->>FileSystem: Save file to ./uploads/avatars/
    Backend->>Service: 200 OK + { avatarUrl }
    end

    rect rgba(76, 175, 80, 0.5)
    Service->>Frontend: Return avatarUrl
    Frontend->>Frontend: Update avatar URL in state (optimistic)
    Frontend->>Service: updateProfile({ avatarUrl })
    Service->>Backend: Persist profile update
    Backend->>Frontend: 200 OK
    User->>Frontend: See updated avatar
    end
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly Related PRs

Suggested Reviewers

  • bhavik-mangla

Poem

🐰 I hopped along the server logs today,
Files found a home where avatars may stay.
Multipart carrots, five megabytes so neat,
Saved with UUIDs, a visual treat.
Hop, click, and share — a new face to display! 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main feature being added: local profile avatar upload with UI integration.
Linked Issues check ✅ Passed The PR implementation fully satisfies all coding requirements from issue #279: backend controller with file validation and size limits, static file serving, protected route registration, frontend upload service, and UI integration.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the avatar upload feature specified in issue #279; no extraneous modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/cmd/server/main.go`:
- Around line 69-71: The two os.MkdirAll calls currently ignore errors; replace
them with a single checked call to os.MkdirAll("uploads/avatars", os.ModePerm)
and handle its error immediately (e.g., log the error with context and exit
non‑zero) so the server fails fast on permission/FS issues; update code around
the main startup (where os.MkdirAll is called) to perform the check and call
log.Fatalf or os.Exit(1) with a clear message when the mkdir fails.

In `@backend/controllers/upload_controller.go`:
- Around line 47-49: The code currently hardcodes baseURL
("http://localhost:1313") when building publicURL, which persists broken links;
update the upload handler to not persist "localhost:1313" by deriving the base
URL from the incoming request or server config instead (e.g., use r.Host and
request scheme or the application's cfg.Server settings) or store and return a
relative path ("/uploads/avatars/%s") so links remain valid across
ports/domains; change the construction of publicURL (which uses baseURL and
newFilename) to use the request-derived host/scheme or a relative URL and ensure
newFilename remains the filename component used to build the path.

In `@frontend/src/Pages/Profile.tsx`:
- Around line 182-218: The file input's onChange handler handleFileUpload does
not reset the hidden file input, so selecting the same file twice won't
retrigger onChange; update handleFileUpload to clear the input value (e.g. reset
e.target.value or use a ref to set input.value = '') after every attempt and
after each early return (validation failure, missing token, success, and catch)
so the input is cleared regardless of outcome; locate handleFileUpload,
uploadAvatar, updateProfile, setErrorMessage, and setSuccessMessage to apply the
reset in all control paths and ensure the file input (the element wired to
onChange) is reset so the same file can be reselected.
- Around line 197-216: The optimistic update sets dashboard.profile.avatarUrl
before ensuring persistence; if uploadAvatar succeeds but updateProfile fails
the UI still shows the new avatar—either move the setDashboard call until after
both uploadAvatar and updateProfile succeed, or capture the previous avatarUrl
(from dashboard.profile.avatarUrl) before updating and in the catch block revert
via setDashboard(...) and call setErrorMessage; specifically update the flow
around uploadAvatar, updateProfile, setDashboard, setSuccessMessage and
setErrorMessage so you restore the prior avatar on failure (or only set the new
avatar after updateProfile returns successfully).
- Around line 816-840: The overlay actions are currently hidden via "opacity-0
group-hover:opacity-100" which makes them inaccessible on touch and keyboard;
change the container's classes to include focus visibility and pointer events
(e.g., replace "opacity-0 group-hover:opacity-100 transition-opacity" with
"opacity-0 group-hover:opacity-100 group-focus-within:opacity-100
transition-opacity pointer-events-none group-hover:pointer-events-auto
group-focus-within:pointer-events-auto") so the controls become visible and
interactive when the avatar container receives focus, ensure the buttons that
call setIsAvatarModalOpen and fileInputRef.current?.click remain native <button>
elements (they already are) so they are tabbable, and add explicit accessible
labels (aria-label or title) to the upload/create buttons and the hidden file
input (and ensure handleFileUpload remains used onChange) so touch and keyboard
users can discover and activate the avatar actions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f96d8f8e-b966-4399-ba86-7060cae7cff6

📥 Commits

Reviewing files that changed from the base of the PR and between 09ef1bb and 54d4c3a.

📒 Files selected for processing (5)
  • backend/cmd/server/main.go
  • backend/controllers/upload_controller.go
  • backend/routes/profile.go
  • frontend/src/Pages/Profile.tsx
  • frontend/src/services/profileService.ts

Comment on lines +816 to +840
<div className="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity gap-3">
<button
type="button"
onClick={() => setIsAvatarModalOpen(true)}
title="Create Avatar"
className="text-white hover:text-primary transition-colors"
>
<Pen className="w-5 h-5" />
</button>
<button
type="button"
onClick={() => fileInputRef.current?.click()}
title="Upload Avatar"
className="text-white hover:text-primary transition-colors"
>
<ImageIcon className="w-5 h-5" />
</button>
</div>
<input
type="file"
ref={fileInputRef}
className="hidden"
accept=".jpg,.jpeg,.png"
onChange={handleFileUpload}
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't make the new avatar actions hover-only.

On touch devices there is no hover, and keyboard users can land on invisible controls. That makes the upload/custom-avatar actions effectively undiscoverable.

Suggested fix
-            <div className="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity gap-3">
+            <div className="absolute inset-0 bg-black/50 flex items-center justify-center gap-3 opacity-100 sm:opacity-0 sm:pointer-events-none sm:group-hover:opacity-100 sm:group-hover:pointer-events-auto sm:group-focus-within:opacity-100 sm:group-focus-within:pointer-events-auto transition-opacity">
               <button
                 type="button"
                 onClick={() => setIsAvatarModalOpen(true)}
                 title="Create Avatar"
+                aria-label="Create avatar"
                 className="text-white hover:text-primary transition-colors"
               >
                 <Pen className="w-5 h-5" />
               </button>
               <button
                 type="button"
                 onClick={() => fileInputRef.current?.click()}
                 title="Upload Avatar"
+                aria-label="Upload avatar"
                 className="text-white hover:text-primary transition-colors"
               >
                 <ImageIcon className="w-5 h-5" />
               </button>
             </div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity gap-3">
<button
type="button"
onClick={() => setIsAvatarModalOpen(true)}
title="Create Avatar"
className="text-white hover:text-primary transition-colors"
>
<Pen className="w-5 h-5" />
</button>
<button
type="button"
onClick={() => fileInputRef.current?.click()}
title="Upload Avatar"
className="text-white hover:text-primary transition-colors"
>
<ImageIcon className="w-5 h-5" />
</button>
</div>
<input
type="file"
ref={fileInputRef}
className="hidden"
accept=".jpg,.jpeg,.png"
onChange={handleFileUpload}
/>
<div className="absolute inset-0 bg-black/50 flex items-center justify-center gap-3 opacity-100 sm:opacity-0 sm:pointer-events-none sm:group-hover:opacity-100 sm:group-hover:pointer-events-auto sm:group-focus-within:opacity-100 sm:group-focus-within:pointer-events-auto transition-opacity">
<button
type="button"
onClick={() => setIsAvatarModalOpen(true)}
title="Create Avatar"
aria-label="Create avatar"
className="text-white hover:text-primary transition-colors"
>
<Pen className="w-5 h-5" />
</button>
<button
type="button"
onClick={() => fileInputRef.current?.click()}
title="Upload Avatar"
aria-label="Upload avatar"
className="text-white hover:text-primary transition-colors"
>
<ImageIcon className="w-5 h-5" />
</button>
</div>
<input
type="file"
ref={fileInputRef}
className="hidden"
accept=".jpg,.jpeg,.png"
onChange={handleFileUpload}
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/Pages/Profile.tsx` around lines 816 - 840, The overlay actions
are currently hidden via "opacity-0 group-hover:opacity-100" which makes them
inaccessible on touch and keyboard; change the container's classes to include
focus visibility and pointer events (e.g., replace "opacity-0
group-hover:opacity-100 transition-opacity" with "opacity-0
group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity
pointer-events-none group-hover:pointer-events-auto
group-focus-within:pointer-events-auto") so the controls become visible and
interactive when the avatar container receives focus, ensure the buttons that
call setIsAvatarModalOpen and fileInputRef.current?.click remain native <button>
elements (they already are) so they are tabbable, and add explicit accessible
labels (aria-label or title) to the upload/create buttons and the hidden file
input (and ensure handleFileUpload remains used onChange) so touch and keyboard
users can discover and activate the avatar actions.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
frontend/src/Pages/Profile.tsx (2)

835-854: ⚠️ Potential issue | 🟡 Minor

Accessibility: Add pointer-events handling for touch and keyboard users.

The overlay now has focus-within:opacity-100 for keyboard visibility, but the buttons remain non-interactive when hidden due to missing pointer-events classes. On touch devices, the hover state never activates, making the buttons undiscoverable.

Proposed fix
-            <div className="absolute inset-0 bg-black/50 flex items-center justify-center gap-3 opacity-0 group-hover:opacity-100 focus-within:opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity">
+            <div className="absolute inset-0 bg-black/50 flex items-center justify-center gap-3 opacity-100 pointer-events-auto sm:opacity-0 sm:pointer-events-none sm:group-hover:opacity-100 sm:group-hover:pointer-events-auto sm:group-focus-within:opacity-100 sm:group-focus-within:pointer-events-auto transition-opacity">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/Pages/Profile.tsx` around lines 835 - 854, The overlay div
currently becomes visible via opacity but its children remain non-interactive;
update the overlay's className to toggle pointer events alongside opacity (use
pointer-events-none when hidden and pointer-events-auto when visible) by adding
the Tailwind variants for group-hover:pointer-events-auto and
focus-within:pointer-events-auto (and keep pointer-events-none as the
default/for sm breakpoint where needed) so the buttons (onClick handlers
setIsAvatarModalOpen and fileInputRef.current?.click) are reachable by keyboard
and touch users.

182-237: ⚠️ Potential issue | 🟡 Minor

Capture previousAvatarUrl before the upload call, not after.

On line 203, previousAvatarUrl is captured after uploadAvatar() succeeds. If uploadAvatar() throws, the outer catch block (lines 232-233) executes but previousAvatarUrl is never assigned, so no revert is possible. Move the capture before the try block.

Proposed fix
   const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
     const input = e.currentTarget;
     const file = input.files?.[0];
     if (!file) return;
+    const previousAvatarUrl = dashboard?.profile.avatarUrl;

     if (file.size > 5 * 1024 * 1024) {
       setErrorMessage("File size should not exceed 5MB.");
@@ -200,7 +201,6 @@
     try {
       const response = await uploadAvatar(token, file);
       const newAvatarUrl = response.avatarUrl;
-      const previousAvatarUrl = dashboard?.profile.avatarUrl;
       
       setDashboard((current: DashboardData | null) => 
         current ? {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/Pages/Profile.tsx` around lines 182 - 237, In handleFileUpload,
previousAvatarUrl is captured too late (after uploadAvatar) so if uploadAvatar
throws you cannot revert; move the assignment const previousAvatarUrl =
dashboard?.profile.avatarUrl to before the try that calls uploadAvatar (capture
the current dashboard.profile avatarUrl immediately after validating token and
file), then use that captured previousAvatarUrl in the inner catch to restore
state after a failed updateProfile; ensure you still handle dashboard possibly
being null when capturing.
🧹 Nitpick comments (1)
backend/cmd/server/main.go (1)

86-87: Static file serving looks correct, but consider placement relative to CORS.

The static route is registered before the CORS middleware. If frontend JavaScript needs to fetch avatars via fetch() with credentials or custom headers, CORS preflight may fail. Currently this works because <img> tags don't trigger CORS, but if you ever need programmatic access from the frontend, you may need to move this after CORS middleware.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/cmd/server/main.go` around lines 86 - 87, The static route
registration router.Static("/uploads", "./uploads") is placed before the CORS
middleware and can cause CORS preflight failures for programmatic frontend
fetches; move this router.Static call so it is registered after the CORS
middleware registration (i.e., after the router.Use(...) call that sets up CORS)
so the uploads route is covered by the CORS rules.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@frontend/src/Pages/Profile.tsx`:
- Around line 835-854: The overlay div currently becomes visible via opacity but
its children remain non-interactive; update the overlay's className to toggle
pointer events alongside opacity (use pointer-events-none when hidden and
pointer-events-auto when visible) by adding the Tailwind variants for
group-hover:pointer-events-auto and focus-within:pointer-events-auto (and keep
pointer-events-none as the default/for sm breakpoint where needed) so the
buttons (onClick handlers setIsAvatarModalOpen and fileInputRef.current?.click)
are reachable by keyboard and touch users.
- Around line 182-237: In handleFileUpload, previousAvatarUrl is captured too
late (after uploadAvatar) so if uploadAvatar throws you cannot revert; move the
assignment const previousAvatarUrl = dashboard?.profile.avatarUrl to before the
try that calls uploadAvatar (capture the current dashboard.profile avatarUrl
immediately after validating token and file), then use that captured
previousAvatarUrl in the inner catch to restore state after a failed
updateProfile; ensure you still handle dashboard possibly being null when
capturing.

---

Nitpick comments:
In `@backend/cmd/server/main.go`:
- Around line 86-87: The static route registration router.Static("/uploads",
"./uploads") is placed before the CORS middleware and can cause CORS preflight
failures for programmatic frontend fetches; move this router.Static call so it
is registered after the CORS middleware registration (i.e., after the
router.Use(...) call that sets up CORS) so the uploads route is covered by the
CORS rules.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e05e1e87-7140-4b38-b3ae-64fc111848bd

📥 Commits

Reviewing files that changed from the base of the PR and between 54d4c3a and 7ef8143.

📒 Files selected for processing (3)
  • backend/cmd/server/main.go
  • backend/controllers/upload_controller.go
  • frontend/src/Pages/Profile.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/controllers/upload_controller.go

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.

Feature Request: Profile Image Upload (Local File Support)

1 participant