Skip to content

feat: add custom avatar upload functionality: #279#321

Open
vinisha1014 wants to merge 1 commit intoAOSSIE-Org:mainfrom
vinisha1014:feat/custom-avatar-upload
Open

feat: add custom avatar upload functionality: #279#321
vinisha1014 wants to merge 1 commit intoAOSSIE-Org:mainfrom
vinisha1014:feat/custom-avatar-upload

Conversation

@vinisha1014
Copy link

@vinisha1014 vinisha1014 commented Feb 15, 2026

Description

Adds custom avatar upload functionality allowing authenticated users to upload their own profile picture (JPG/PNG, max 5MB). The uploaded avatar is stored on the backend, served statically, and persisted in MongoDB. The existing DiceBear avatar system is fully preserved as a fallback.

Changes
### Backend:

New upload_controller.go — Handles multipart upload with file size (5MB) and type validation (extension + MIME sniffing), UUID filenames, saves to ./uploads/avatars/, updates avatarUrl in MongoDB
Modified main.go — Added router.Static("/uploads", "./uploads") for static serving + registered JWT-protected POST /user/upload-avatar route

### Frontend:

Modified profileService.ts — Added uploadAvatar() using FormData
Modified Profile.tsx — Added file input, client-side validation, upload spinner, instant avatar update on success
Modified AvatarModal.tsx — Added "Upload Photo" tab alongside existing DiceBear customizer with file preview and upload handling

Key Decisions

No new dependencies — uses Gin's built-in multipart support and existing google/uuid package
Dual validation: extension whitelist + MIME type detection via http.DetectContentType()
DiceBear fallback untouched — no regression for users without custom avatars
Both go build and vite build pass with zero errors

Screenshots/Recordings

image image image

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

Summary by CodeRabbit

Release Notes

  • New Features
    • Users can now upload custom avatar photos to their profile (JPG/PNG formats, 5 MB maximum)
    • Upload interface includes real-time loading feedback and clear error messaging
    • Avatar customization options remain available alongside the new photo upload capability

- Add upload controller (backend/controllers/upload_controller.go)
  - Multipart file upload with 5MB limit
  - JPG/PNG validation (extension + MIME type)
  - UUID-based unique filenames
  - Saves to ./uploads/avatars/
  - Updates user avatarUrl in MongoDB

- Update server entry point (backend/cmd/server/main.go)
  - Serve static files from /uploads
  - Register POST /user/upload-avatar route (JWT protected)

- Add uploadAvatar service (frontend/src/services/profileService.ts)
  - FormData-based file upload with proper error handling

- Update Profile page (frontend/src/Pages/Profile.tsx)
  - File input with client-side validation
  - Loading spinner during upload
  - Immediate avatar update on success

- Update AvatarModal (frontend/src/components/AvatarModal.tsx)
  - Add 'Upload Photo' tab alongside existing DiceBear customization
  - File preview, upload with spinner, error handling
  - DiceBear fallback preserved
Copilot AI review requested due to automatic review settings February 15, 2026 16:42
@coderabbitai
Copy link

coderabbitai bot commented Feb 15, 2026

📝 Walkthrough

Walkthrough

This pull request introduces an avatar upload feature across the application stack. It adds a backend HTTP endpoint for authenticated avatar uploads with file validation (size, type), a new API service function to handle uploads, and frontend UI components enabling users to upload avatars with real-time preview and error handling.

Changes

Cohort / File(s) Summary
Backend Server Setup
backend/cmd/server/main.go
Added controllers package import and exposed authenticated POST /user/upload-avatar route.
Backend Avatar Upload Handler
backend/controllers/upload_controller.go
Implemented UploadAvatar handler with authentication verification, 5 MB file size limit, JPG/PNG MIME type validation, UUID-based file naming, and MongoDB user record updates; includes validateMIMEType helper.
Frontend Avatar Upload Service
frontend/src/services/profileService.ts
Added uploadAvatar function to POST avatar files to /user/upload-avatar endpoint with Authorization header and error handling for file size (413) and general failures.
Frontend Profile Page UI
frontend/src/Pages/Profile.tsx
Integrated avatar upload flow with file input ref, loading state, async handler with client-side validation, and optimistic UI updates; added Upload Photo and Choose Avatar actions with loading spinner overlay.
Frontend Avatar Customization Modal
frontend/src/components/AvatarModal.tsx
Added tab-based interface for avatar selection with new Upload tab featuring file preview, authentication-aware upload submission, error display, and loading indicator; preserved existing customize logic.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client (Browser)
    participant FE as Frontend Service
    participant Server as Backend Server
    participant FS as File System
    participant DB as MongoDB

    Client->>FE: Select avatar file
    FE->>FE: Validate file (type, size)
    FE->>FE: Get auth token
    FE->>FE: Create FormData with file
    FE->>Server: POST /user/upload-avatar
    Server->>Server: Verify authentication
    Server->>Server: Validate file size & MIME type
    Server->>FS: Create /uploads directory (if needed)
    Server->>FS: Save file with UUID name
    Server->>DB: Update user avatarUrl & updatedAt
    DB-->>Server: User record updated
    Server-->>FE: Return avatar_url + message
    FE->>Client: Update UI with new avatar
    FE->>Client: Clear file input
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly Related PRs

Suggested Reviewers

  • bhavik-mangla

Poem

🐰 Hops of joy, files now fly!
Avatars upload, no need to shy.
With validation and a UUID gleam,
Profile pictures fuel the dream! ✨📸

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and directly describes the main feature being added: custom avatar upload functionality, matching the primary changes across all modified files.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds end-to-end custom avatar upload support (JWT-protected) so users can upload a JPG/PNG avatar (max 5MB), persist it in MongoDB, and serve it via the backend while keeping DiceBear as fallback.

Changes:

  • Backend: add UploadAvatar controller with validation + disk persistence; register JWT route and static serving for uploaded files.
  • Frontend: add uploadAvatar() API call; add upload UI/flow in Profile and as a new “Upload Photo” tab in AvatarModal.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
frontend/src/services/profileService.ts Adds uploadAvatar() API client using FormData and JWT.
frontend/src/components/AvatarModal.tsx Adds “Upload Photo” tab with preview and upload handling.
frontend/src/Pages/Profile.tsx Adds hover actions + inline avatar upload with loading spinner.
backend/controllers/upload_controller.go New multipart upload endpoint with validation and DB update.
backend/cmd/server/main.go Serves /uploads statically and registers POST /user/upload-avatar.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +45 to +52
file, header, err := c.Request.FormFile("avatar")
if err != nil {
if err.Error() == "http: request body too large" {
c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "File too large. Maximum size is 5MB."})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": "No file provided or invalid upload. Use key 'avatar'."})
return
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

The request-size error check relies on comparing err.Error() to a specific string, which is brittle (the error can be wrapped or have a different message). Prefer detecting *http.MaxBytesError via errors.As (or similar) so oversized uploads reliably return 413.

Copilot uses AI. Check for mistakes.
Comment on lines +94 to +95
// Build the public URL
avatarURL := fmt.Sprintf("http://localhost:1313%s/%s", avatarURLPrefix, uniqueName)
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

avatarURL is built with a hardcoded http://localhost:1313 base, which will break in production and in any non-default dev setup. Consider building the URL from the incoming request (scheme/host, respecting reverse-proxy headers) or storing a relative path like /uploads/avatars/<file> and letting the frontend/baseURL resolve it.

Suggested change
// Build the public URL
avatarURL := fmt.Sprintf("http://localhost:1313%s/%s", avatarURLPrefix, uniqueName)
// Build the public URL as a relative path; let the frontend/base URL resolve the host and scheme.
avatarURL := fmt.Sprintf("%s/%s", avatarURLPrefix, uniqueName)

Copilot uses AI. Check for mistakes.
Comment on lines +442 to +445
setUploadError('');
setUploadFile(file);
setUploadPreview(URL.createObjectURL(file));
};
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

URL.createObjectURL(file) is used to generate a preview, but the URL is never revoked. This can leak memory if the modal is opened/closed or the user selects multiple files. Revoke the previous object URL when replacing it and when the modal unmounts/closes (e.g., via an effect cleanup).

Copilot uses AI. Check for mistakes.
Comment on lines +434 to +438
setUploadError('Only JPG and PNG files are allowed.');
return;
}
if (file.size > 5 * 1024 * 1024) {
setUploadError('File too large. Maximum size is 5MB.');
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

On invalid file type/size, handleFileSelect sets an error but doesn’t clear the previous uploadFile/uploadPreview or reset the underlying <input>. This can leave a stale preview selected and can prevent re-selecting the same file (no onChange fire). Consider clearing state (and resetting the input’s value) on validation failure.

Suggested change
setUploadError('Only JPG and PNG files are allowed.');
return;
}
if (file.size > 5 * 1024 * 1024) {
setUploadError('File too large. Maximum size is 5MB.');
setUploadError('Only JPG and PNG files are allowed.');
setUploadFile(null);
setUploadPreview(null);
e.target.value = '';
return;
}
if (file.size > 5 * 1024 * 1024) {
setUploadError('File too large. Maximum size is 5MB.');
setUploadFile(null);
setUploadPreview(null);
e.target.value = '';

Copilot uses AI. Check for mistakes.
Comment on lines +324 to +342
// Validate file type
const allowedTypes = ["image/jpeg", "image/png"];
if (!allowedTypes.includes(file.type)) {
setErrorMessage("Invalid file type. Only JPG and PNG files are allowed.");
return;
}

// Validate file size (5MB)
const MAX_SIZE = 5 * 1024 * 1024;
if (file.size > MAX_SIZE) {
setErrorMessage("File too large. Maximum size is 5MB.");
return;
}

const token = getAuthToken();
if (!token) {
setErrorMessage("Authentication token is missing.");
return;
}
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

When avatar upload validation fails (type/size/token) the handler returns early without resetting the hidden file input value, so choosing the same file again may not trigger onChange. Reset fileInputRef.current.value (and possibly clear any prior error) on these early-return paths too.

Copilot uses AI. Check for mistakes.
Comment on lines +111 to +116
// File was saved but DB failed — still return the URL so the frontend can retry
c.JSON(http.StatusInternalServerError, gin.H{"error": "Avatar saved but failed to update profile. Please try again."})
return
}
if result.MatchedCount == 0 {
log.Printf("UploadAvatar: No user found with email: %s", email)
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

If the DB update fails or no user is matched, the avatar file has already been written to disk and is never cleaned up, leading to orphaned files accumulating over time. Either remove the saved file on these failure paths (and/or delete the user’s previous custom avatar when replacing it) or restructure the flow to avoid committing the file until the DB update succeeds.

Suggested change
// File was saved but DB failed — still return the URL so the frontend can retry
c.JSON(http.StatusInternalServerError, gin.H{"error": "Avatar saved but failed to update profile. Please try again."})
return
}
if result.MatchedCount == 0 {
log.Printf("UploadAvatar: No user found with email: %s", email)
// DB update failed; remove the saved file to avoid leaving an orphaned avatar on disk.
if removeErr := os.Remove(savePath); removeErr != nil {
log.Printf("UploadAvatar: Failed to remove orphaned avatar file %s: %v", savePath, removeErr)
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Avatar saved but failed to update profile. Please try again."})
return
}
if result.MatchedCount == 0 {
log.Printf("UploadAvatar: No user found with email: %s", email)
// No user matched; remove the saved file to avoid leaving an orphaned avatar on disk.
if removeErr := os.Remove(savePath); removeErr != nil {
log.Printf("UploadAvatar: Failed to remove orphaned avatar file %s: %v", savePath, removeErr)
}

Copilot uses AI. Check for mistakes.
if err != nil {
log.Printf("UploadAvatar: Failed to update user avatar in DB: %v", err)
// File was saved but DB failed — still return the URL so the frontend can retry
c.JSON(http.StatusInternalServerError, gin.H{"error": "Avatar saved but failed to update profile. Please try again."})
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

The comment says “still return the URL so the frontend can retry” but the response on DB failure only returns an error field (no URL). Please either include the URL in the error response (if that’s the intended behavior) or update the comment and clean up the saved file so the system doesn’t get into a half-written state.

Suggested change
c.JSON(http.StatusInternalServerError, gin.H{"error": "Avatar saved but failed to update profile. Please try again."})
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Avatar saved but failed to update profile. Please try again.",
"avatar_url": avatarURL,
})

Copilot uses AI. Check for mistakes.
Comment on lines +124 to +125
"message": "Avatar uploaded successfully",
"avatar_url": avatarURL,
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

This endpoint returns avatar_url (snake_case), while the rest of the API uses avatarUrl (camelCase) for the same field (e.g., profile responses). For consistency and to reduce client-side special casing, return avatarUrl here as well (and align the frontend typings/usages accordingly).

Suggested change
"message": "Avatar uploaded successfully",
"avatar_url": avatarURL,
"message": "Avatar uploaded successfully",
"avatarUrl": avatarURL,

Copilot uses AI. Check for mistakes.
Comment on lines +131 to +135
buf := make([]byte, 512)
n, err := file.Read(buf)
if err != nil {
return false
}
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

validateMIMEType returns false on any read error. For small files, Read can return n > 0 with err == io.EOF, which would incorrectly reject otherwise valid images. Consider treating io.EOF as non-fatal when n > 0 (and import/use io).

Copilot uses AI. Check for mistakes.
Copy link

@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: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/cmd/server/main.go (1)

70-71: ⚠️ Potential issue | 🟡 Minor

MkdirAll error silently discarded and permissions are overly broad.

If directory creation fails, the server will start but all uploads will fail with confusing errors. Log or fail fast.

Proposed fix
-	// Create uploads directory
-	os.MkdirAll("uploads", os.ModePerm)
+	// Create uploads directory
+	if err := os.MkdirAll("uploads", 0750); err != nil {
+		log.Fatalf("Failed to create uploads directory: %v", err)
+	}
🤖 Fix all issues with AI agents
In `@backend/controllers/upload_controller.go`:
- Around line 115-121: The logs in UploadAvatar currently print the user's email
(variables email) on both failure and success paths (checked via
result.MatchedCount and avatarURL); replace those plaintext emails with a
non-PII identifier such as the user's ID (if available on the user object) or a
hashed/truncated version of email (e.g., SHA256 or first/last 4 chars) before
logging so logs no longer contain full PII. Locate the UploadAvatar handler and
update the log.Printf calls to use the chosen safe identifier (derived from
email or using user.ID) while keeping the rest of the messages unchanged.
- Around line 101-127: In the UploadAvatar handler, prevent orphaned avatar
files by reading the user's existing avatarUrl before saving the new file: query
the users collection (e.g., with db.MongoDatabase.Collection("users").FindOne
using the same email/dbCtx), extract the existing filename from the returned
avatarUrl, and delete that file from disk (os.Remove) if it exists and is not
the default placeholder; then proceed to save the new UUID-named file and run
the current UpdateOne call that sets "avatarUrl" and "updatedAt". Ensure
deletion errors are logged but do not block the new upload, and avoid deleting
if avatarUrl is empty or matches a shared default.
- Around line 130-148: The validateMIMEType function currently ignores whether
the uploaded file supports seeking and ignores seek errors—change the logic in
validateMIMEType to assert the file to io.Seeker (or equivalent interface), and
if the assertion fails or seeker.Seek(0, io.SeekStart) returns an error, return
false immediately; this ensures readers like c.SaveUploadedFile will see the
file from the start and prevents saving corrupted uploads. Keep the rest of the
MIME detection flow (reading 512 bytes, DetectContentType, comparing against
allowedMIMEs) unchanged.
- Line 95: Replace the hardcoded "http://localhost:1313" used to build avatarURL
with a dynamic base URL: read the scheme and host from the incoming
*http.Request (request.URL.Scheme or infer via r.TLS and r.Host) or, preferably,
a configurable base URL from your app config, and then join that base with
avatarURLPrefix and uniqueName; update the avatar URL construction (the
avatarURL variable) to use this computed base URL and include a sensible
fallback if neither request-derived host nor config is available.
- Around line 46-53: The current error check compares err.Error() to a string;
instead use errors.As to detect *http.MaxBytesError and handle it specially: in
the upload handler where err is returned (the block using c.JSON), attempt var
maxErr *http.MaxBytesError; if errors.As(err,&maxErr) respond with
http.StatusRequestEntityTooLarge and the same "File too large. Maximum size is
5MB." message, otherwise fall back to the generic BadRequest response; ensure
you import the errors package and reference *http.MaxBytesError and the handler
function that processes the upload (the block currently checking err and calling
c.JSON).

In `@frontend/src/components/AvatarModal.tsx`:
- Around line 428-445: The code in handleFileSelect currently calls
URL.createObjectURL and never revokes it, causing a memory leak; before calling
setUploadPreview(URL.createObjectURL(file)) revoke any existing blob URL (check
the uploadPreview state) via URL.revokeObjectURL(uploadPreview), then set the
new preview, and also add a cleanup (e.g., in an effect that watches
uploadPreview or on modal close/unmount) that revokes the current uploadPreview
when the component unmounts or the modal closes to ensure all created object
URLs are released.
🧹 Nitpick comments (2)
backend/controllers/upload_controller.go (1)

77-77: os.ModePerm (0777) is overly permissive for the upload directory.

This grants read/write/execute to all users on the system. Use a more restrictive permission like 0750.

Proposed fix
-	if err := os.MkdirAll(avatarUploadDir, os.ModePerm); err != nil {
+	if err := os.MkdirAll(avatarUploadDir, 0750); err != nil {
frontend/src/components/AvatarModal.tsx (1)

25-30: Upload state persists across modal open/close cycles.

If the user selects a file, closes the modal without uploading, then reopens it, they'll see stale preview/error state. Reset upload-related state when the modal opens.

Proposed fix — add a reset effect
useEffect(() => {
  if (isOpen) {
    setActiveTab('customize');
    setUploadPreview(null);
    setUploadFile(null);
    setUploadError('');
  }
}, [isOpen]);

Comment on lines +46 to +53
if err != nil {
if err.Error() == "http: request body too large" {
c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "File too large. Maximum size is 5MB."})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": "No file provided or invalid upload. Use key 'avatar'."})
return
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Go http.MaxBytesError type documentation

💡 Result:

http.MaxBytesError is an error type in Go’s standard library package net/http (added in Go 1.19). It’s returned by http.MaxBytesReader when the request body exceeds the configured byte limit. It has a single field Limit int64 and implements error via (*MaxBytesError).Error(). [1], [2]

Docs: see the net/http package documentation on pkg.go.dev (entry type MaxBytesError). [1]

Related note (Go 1.19 release notes): MaxBytesReader now returns this defined error type when the limit is exceeded. [2]

Sources:
[1] pkg.go.dev net/http (type MaxBytesError) (pkg.go.dev)
[2] Go 1.19 release notes (net/http section) (tip.golang.org)

Citations:


🌐 Web query:

Go FormFile large file error handling http.Request

💡 Result:

(*http.Request).FormFile() is convenient but it forces multipart parsing (it calls ParseMultipartForm/ParseForm) and therefore will read/parse the request body rather than letting you “peek” safely first. (pkg.go.dev)

1) Put a hard cap on upload size (recommended)

Wrap the request body before calling ParseMultipartForm / FormFile:

func upload(w http.ResponseWriter, r *http.Request) {
    const maxBody = 50 << 20 // 50 MiB (whole request, not just the file)

    r.Body = http.MaxBytesReader(w, r.Body, maxBody)

    if err := r.ParseMultipartForm(32 << 20); err != nil {
        var mbe *http.MaxBytesError
        if errors.As(err, &mbe) {
            http.Error(w, "upload too large", http.StatusRequestEntityTooLarge) // 413
            return
        }
        http.Error(w, "bad multipart form", http.StatusBadRequest)
        return
    }

    f, fh, err := r.FormFile("file")
    if err != nil {
        http.Error(w, "missing/invalid file", http.StatusBadRequest)
        return
    }
    defer f.Close()

    // Optional per-file check (not a security boundary by itself):
    if fh.Size > (40 << 20) {
        http.Error(w, "file too large", http.StatusRequestEntityTooLarge)
        return
    }

    // stream/save f...
}

Why this works:

  • http.MaxBytesReader enforces a hard limit and returns a non-nil error (type *http.MaxBytesError) once the limit is exceeded. (pkg.go.dev)
  • Without this, ParseMultipartForm(maxMemory) mainly controls memory buffering (rest spills to temp files) and is not a hard upload-size limit by itself. (pkg.go.dev)
  • When the limit is hit, you’ll often see errors like multipart: NextPart: http: request body too large. (stackoverflow.com)

2) For truly large files: stream instead of FormFile

Use r.MultipartReader() and process parts sequentially (e.g., io.CopyN to enforce per-file limits) to avoid buffering the whole form. (pkg.go.dev)

Citations:


🏁 Script executed:

grep -n "MaxBytesReader\|c.Request.Body\|FormFile" backend/controllers/upload_controller.go | head -20

Repository: AOSSIE-Org/DebateAI

Length of output: 320


🏁 Script executed:

git ls-files backend/ | grep -E "(upload|controller)" | head -10

Repository: AOSSIE-Org/DebateAI

Length of output: 465


🏁 Script executed:

wc -l backend/controllers/upload_controller.go

Repository: AOSSIE-Org/DebateAI

Length of output: 107


🏁 Script executed:

sed -n '40,60p' backend/controllers/upload_controller.go

Repository: AOSSIE-Org/DebateAI

Length of output: 811


🏁 Script executed:

sed -n '1,30p' backend/controllers/upload_controller.go

Repository: AOSSIE-Org/DebateAI

Length of output: 611


Use errors.As to check for http.MaxBytesError instead of string comparison.

The string comparison err.Error() == "http: request body too large" is fragile and can vary across Go versions. Since http.MaxBytesReader is already properly set up on line 43, it will return *http.MaxBytesError when the limit is exceeded. Use errors.As to check for this error type instead.

Proposed fix
+	"errors"
 ...
 	file, header, err := c.Request.FormFile("avatar")
 	if err != nil {
-		if err.Error() == "http: request body too large" {
+		var maxBytesErr *http.MaxBytesError
+		if errors.As(err, &maxBytesErr) {
 			c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "File too large. Maximum size is 5MB."})
 			return
 		}
📝 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
if err != nil {
if err.Error() == "http: request body too large" {
c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "File too large. Maximum size is 5MB."})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": "No file provided or invalid upload. Use key 'avatar'."})
return
}
if err != nil {
var maxBytesErr *http.MaxBytesError
if errors.As(err, &maxBytesErr) {
c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "File too large. Maximum size is 5MB."})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": "No file provided or invalid upload. Use key 'avatar'."})
return
}
🤖 Prompt for AI Agents
In `@backend/controllers/upload_controller.go` around lines 46 - 53, The current
error check compares err.Error() to a string; instead use errors.As to detect
*http.MaxBytesError and handle it specially: in the upload handler where err is
returned (the block using c.JSON), attempt var maxErr *http.MaxBytesError; if
errors.As(err,&maxErr) respond with http.StatusRequestEntityTooLarge and the
same "File too large. Maximum size is 5MB." message, otherwise fall back to the
generic BadRequest response; ensure you import the errors package and reference
*http.MaxBytesError and the handler function that processes the upload (the
block currently checking err and calling c.JSON).

}

// Build the public URL
avatarURL := fmt.Sprintf("http://localhost:1313%s/%s", avatarURLPrefix, uniqueName)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Hardcoded localhost:1313 URL will break in any non-local deployment.

The avatar URL stored in MongoDB will be unusable in staging/production. This should use the request's host/scheme or a configurable base URL from the application config.

Proposed fix
-	avatarURL := fmt.Sprintf("http://localhost:1313%s/%s", avatarURLPrefix, uniqueName)
+	scheme := "http"
+	if c.Request.TLS != nil {
+		scheme = "https"
+	}
+	avatarURL := fmt.Sprintf("%s://%s%s/%s", scheme, c.Request.Host, avatarURLPrefix, uniqueName)

Alternatively, inject a base URL from the application configuration.

📝 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
avatarURL := fmt.Sprintf("http://localhost:1313%s/%s", avatarURLPrefix, uniqueName)
scheme := "http"
if c.Request.TLS != nil {
scheme = "https"
}
avatarURL := fmt.Sprintf("%s://%s%s/%s", scheme, c.Request.Host, avatarURLPrefix, uniqueName)
🤖 Prompt for AI Agents
In `@backend/controllers/upload_controller.go` at line 95, Replace the hardcoded
"http://localhost:1313" used to build avatarURL with a dynamic base URL: read
the scheme and host from the incoming *http.Request (request.URL.Scheme or infer
via r.TLS and r.Host) or, preferably, a configurable base URL from your app
config, and then join that base with avatarURLPrefix and uniqueName; update the
avatar URL construction (the avatarURL variable) to use this computed base URL
and include a sensible fallback if neither request-derived host nor config is
available.

Comment on lines +101 to +127
result, err := db.MongoDatabase.Collection("users").UpdateOne(
dbCtx,
bson.M{"email": email},
bson.M{"$set": bson.M{
"avatarUrl": avatarURL,
"updatedAt": time.Now(),
}},
)
if err != nil {
log.Printf("UploadAvatar: Failed to update user avatar in DB: %v", err)
// File was saved but DB failed — still return the URL so the frontend can retry
c.JSON(http.StatusInternalServerError, gin.H{"error": "Avatar saved but failed to update profile. Please try again."})
return
}
if result.MatchedCount == 0 {
log.Printf("UploadAvatar: No user found with email: %s", email)
c.JSON(http.StatusNotFound, gin.H{"error": "User not found."})
return
}

log.Printf("UploadAvatar: Successfully uploaded avatar for %s → %s", email, avatarURL)

c.JSON(http.StatusOK, gin.H{
"message": "Avatar uploaded successfully",
"avatar_url": avatarURL,
})
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

No cleanup of old avatar files on re-upload — unbounded disk growth.

Each upload creates a new UUID-named file but never deletes the previous avatar. Over time this leaks disk space. Before saving the new file, query the user's current avatarUrl, extract the filename, and delete it from disk.

🤖 Prompt for AI Agents
In `@backend/controllers/upload_controller.go` around lines 101 - 127, In the
UploadAvatar handler, prevent orphaned avatar files by reading the user's
existing avatarUrl before saving the new file: query the users collection (e.g.,
with db.MongoDatabase.Collection("users").FindOne using the same email/dbCtx),
extract the existing filename from the returned avatarUrl, and delete that file
from disk (os.Remove) if it exists and is not the default placeholder; then
proceed to save the new UUID-named file and run the current UpdateOne call that
sets "avatarUrl" and "updatedAt". Ensure deletion errors are logged but do not
block the new upload, and avoid deleting if avatarUrl is empty or matches a
shared default.

Comment on lines +115 to +121
if result.MatchedCount == 0 {
log.Printf("UploadAvatar: No user found with email: %s", email)
c.JSON(http.StatusNotFound, gin.H{"error": "User not found."})
return
}

log.Printf("UploadAvatar: Successfully uploaded avatar for %s → %s", email, avatarURL)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

User email logged in plaintext — PII concern.

Lines 116 and 121 log the user's email address. Consider logging a user ID or a hashed/truncated identifier instead to reduce PII exposure in logs.

🤖 Prompt for AI Agents
In `@backend/controllers/upload_controller.go` around lines 115 - 121, The logs in
UploadAvatar currently print the user's email (variables email) on both failure
and success paths (checked via result.MatchedCount and avatarURL); replace those
plaintext emails with a non-PII identifier such as the user's ID (if available
on the user object) or a hashed/truncated version of email (e.g., SHA256 or
first/last 4 chars) before logging so logs no longer contain full PII. Locate
the UploadAvatar handler and update the log.Printf calls to use the chosen safe
identifier (derived from email or using user.ID) while keeping the rest of the
messages unchanged.

Comment on lines +130 to +148
func validateMIMEType(file multipart.File, allowedMIMEs []string) bool {
buf := make([]byte, 512)
n, err := file.Read(buf)
if err != nil {
return false
}

// Reset reader position for subsequent reads
if seeker, ok := file.(interface{ Seek(int64, int) (int64, error) }); ok {
seeker.Seek(0, 0)
}

detectedType := http.DetectContentType(buf[:n])
for _, mime := range allowedMIMEs {
if detectedType == mime {
return true
}
}
return false
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Seek error silently ignored; consider returning false on failure.

If the file doesn't implement Seek or if Seek fails, subsequent reads (e.g., c.SaveUploadedFile) will read from byte 512 onward, producing a corrupt saved file. At minimum, return false when the seek interface is unavailable or when seek errors.

Proposed fix
 func validateMIMEType(file multipart.File, allowedMIMEs []string) bool {
 	buf := make([]byte, 512)
 	n, err := file.Read(buf)
 	if err != nil {
 		return false
 	}
 
-	// Reset reader position for subsequent reads
-	if seeker, ok := file.(interface{ Seek(int64, int) (int64, error) }); ok {
-		seeker.Seek(0, 0)
+	seeker, ok := file.(interface{ Seek(int64, int) (int64, error) })
+	if !ok {
+		return false
+	}
+	if _, err := seeker.Seek(0, 0); err != nil {
+		return false
 	}
 
 	detectedType := http.DetectContentType(buf[:n])
🤖 Prompt for AI Agents
In `@backend/controllers/upload_controller.go` around lines 130 - 148, The
validateMIMEType function currently ignores whether the uploaded file supports
seeking and ignores seek errors—change the logic in validateMIMEType to assert
the file to io.Seeker (or equivalent interface), and if the assertion fails or
seeker.Seek(0, io.SeekStart) returns an error, return false immediately; this
ensures readers like c.SaveUploadedFile will see the file from the start and
prevents saving corrupted uploads. Keep the rest of the MIME detection flow
(reading 512 bytes, DetectContentType, comparing against allowedMIMEs)
unchanged.

Comment on lines +428 to +445
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;

const allowedTypes = ['image/jpeg', 'image/png'];
if (!allowedTypes.includes(file.type)) {
setUploadError('Only JPG and PNG files are allowed.');
return;
}
if (file.size > 5 * 1024 * 1024) {
setUploadError('File too large. Maximum size is 5MB.');
return;
}

setUploadError('');
setUploadFile(file);
setUploadPreview(URL.createObjectURL(file));
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Object URL memory leak — URL.revokeObjectURL is never called.

Each call to URL.createObjectURL (line 444) allocates a blob URL that persists until the page is unloaded or explicitly revoked. Revoke the previous URL before creating a new one, and on modal close/unmount.

Proposed fix
   const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
     const file = e.target.files?.[0];
     if (!file) return;
 
     const allowedTypes = ['image/jpeg', 'image/png'];
     if (!allowedTypes.includes(file.type)) {
       setUploadError('Only JPG and PNG files are allowed.');
       return;
     }
     if (file.size > 5 * 1024 * 1024) {
       setUploadError('File too large. Maximum size is 5MB.');
       return;
     }
 
     setUploadError('');
     setUploadFile(file);
+    if (uploadPreview) URL.revokeObjectURL(uploadPreview);
     setUploadPreview(URL.createObjectURL(file));
   };

Also add cleanup on modal close or via a useEffect cleanup:

useEffect(() => {
  return () => {
    if (uploadPreview) URL.revokeObjectURL(uploadPreview);
  };
}, [uploadPreview]);
📝 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
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const allowedTypes = ['image/jpeg', 'image/png'];
if (!allowedTypes.includes(file.type)) {
setUploadError('Only JPG and PNG files are allowed.');
return;
}
if (file.size > 5 * 1024 * 1024) {
setUploadError('File too large. Maximum size is 5MB.');
return;
}
setUploadError('');
setUploadFile(file);
setUploadPreview(URL.createObjectURL(file));
};
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const allowedTypes = ['image/jpeg', 'image/png'];
if (!allowedTypes.includes(file.type)) {
setUploadError('Only JPG and PNG files are allowed.');
return;
}
if (file.size > 5 * 1024 * 1024) {
setUploadError('File too large. Maximum size is 5MB.');
return;
}
setUploadError('');
setUploadFile(file);
if (uploadPreview) URL.revokeObjectURL(uploadPreview);
setUploadPreview(URL.createObjectURL(file));
};
🤖 Prompt for AI Agents
In `@frontend/src/components/AvatarModal.tsx` around lines 428 - 445, The code in
handleFileSelect currently calls URL.createObjectURL and never revokes it,
causing a memory leak; before calling
setUploadPreview(URL.createObjectURL(file)) revoke any existing blob URL (check
the uploadPreview state) via URL.revokeObjectURL(uploadPreview), then set the
new preview, and also add a cleanup (e.g., in an effect that watches
uploadPreview or on modal close/unmount) that revokes the current uploadPreview
when the component unmounts or the modal closes to ensure all created object
URLs are released.

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.

2 participants