Skip to content

feat(profile): add avatar upload support using Vercel Blob with client-side upload and WebP conversion#31

Merged
sirily11 merged 7 commits into
mainfrom
copilot/add-avatar-support-user
Mar 6, 2026
Merged

feat(profile): add avatar upload support using Vercel Blob with client-side upload and WebP conversion#31
sirily11 merged 7 commits into
mainfrom
copilot/add-avatar-support-user

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 6, 2026

  • Switch to Vercel Blob client-side upload with 4MB limit
  • Add onUploadCompleted callback for WebP conversion via sharp
  • Update profile form for client-side upload with polling
  • Add sharp dependency for server-side image processing
  • Fix code review feedback (safe ordering, separate success messages, type naming)
  • Fix header avatar not using uploaded avatar URL (account layout)
  • Fix header avatar not displaying as circle (added object-cover and fixed w-8 h-8 dimensions)

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
rxlab-auth Ready Ready Preview, Comment Mar 6, 2026 4:56pm

Request Review

- Add avatarUrl column to users table schema
- Create database migration for avatarUrl column
- Create uploadAvatar and removeAvatar server actions
- Update profile form with avatar upload/remove UI
- Update OAuth userinfo and token endpoints to use avatarUrl
- Add avatar cleanup on user and account deletion
- Update next.config.ts with Vercel Blob image domain
- Add unit tests for avatar actions

Co-authored-by: sirily11 <32106111+sirily11@users.noreply.github.com>
Co-authored-by: sirily11 <32106111+sirily11@users.noreply.github.com>
Copilot AI changed the title [WIP] Add avatar support for user using Vercel Blob Add avatar upload support using Vercel Blob Mar 6, 2026
@sirily11 sirily11 marked this pull request as ready for review March 6, 2026 15:35
Copilot AI review requested due to automatic review settings March 6, 2026 15:35
@autopilot-project-manager autopilot-project-manager Bot changed the title Add avatar upload support using Vercel Blob feat(profile): add avatar upload support using Vercel Blob Mar 6, 2026
@sirily11 sirily11 enabled auto-merge (squash) March 6, 2026 15:38
@sirily11 sirily11 disabled auto-merge March 6, 2026 15:38
Copy link
Copy Markdown

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 first-class user avatar uploads backed by Vercel Blob, while preserving the existing identicon fallback and exposing the uploaded avatar via OAuth picture claims.

Changes:

  • Add users.avatar_url with migration metadata updates.
  • Implement server actions for avatar upload/remove with shared client/server validation constants.
  • Update account profile UI and OAuth userinfo/token responses to prefer uploaded avatars, plus cleanup on user deletion.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
next.config.ts Allows Next image loading from Vercel Blob public hostnames.
lib/db/schema/users.ts Adds avatarUrl column mapping to the users schema.
lib/db/migrations/meta/_journal.json Registers the new migration in Drizzle’s journal.
lib/db/migrations/meta/0005_snapshot.json Captures the updated DB snapshot including avatar_url.
lib/db/migrations/0005_easy_avatar.sql Adds avatar_url to the users table.
lib/constants/avatar.ts Centralizes avatar file constraints for UI + server validation.
components/account/profile-form.tsx Adds avatar upload/remove UI with local preview and loading state.
app/api/oauth/userinfo/route.ts Returns picture as avatarUrl when available.
app/api/oauth/token/route.ts Returns picture as avatarUrl when available in ID token claims.
app/account/page.tsx Passes avatarUrl into the profile form.
actions/admin/users/delete.ts Deletes avatar blob (best-effort) before deleting a user.
actions/account/delete-account.ts Deletes avatar blob (best-effort) before deleting the account.
actions/account/avatar.ts Adds uploadAvatar and removeAvatar server actions with validation + blob operations.
actions/account/avatar.test.ts Adds unit tests for upload/remove flows and validation cases.

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

Comment thread actions/account/avatar.ts Outdated
Comment on lines +58 to +68
// Delete old avatar if exists
if (user.avatarUrl) {
try {
await deleteImage(user.avatarUrl);
} catch {
// Ignore deletion errors for old avatar
}
}

// Upload new avatar
const result = await uploadImage(file, "avatars");
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

uploadAvatar deletes the existing blob before the new upload succeeds. If uploadImage (or the DB update) fails, the user can lose their previously uploaded avatar even though the operation reported failure. Consider uploading first and only deleting the previous blob after the new URL is successfully stored (or do both steps in a transaction-like flow with best-effort cleanup).

Copilot uses AI. Check for mistakes.
Comment thread actions/account/avatar.ts Outdated
Comment on lines +105 to +121
// Delete avatar from blob storage if exists
if (user.avatarUrl) {
try {
await deleteImage(user.avatarUrl);
} catch {
// Ignore deletion errors
}
}

// Clear avatar URL from user record
await db
.update(users)
.set({
avatarUrl: null,
updatedAt: new Date(),
})
.where(eq(users.id, session.userId!));
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

removeAvatar deletes the blob before clearing users.avatarUrl. If the subsequent DB update fails, the account will still reference a now-deleted blob URL (broken image) and you’ve lost the only pointer needed for later cleanup. Consider clearing the DB field first (capturing the old URL to delete afterward) or wrapping the update+delete in a transaction with best-effort cleanup.

Copilot uses AI. Check for mistakes.
Comment thread components/account/profile-form.tsx Outdated
Comment on lines +66 to +72
if (result.success && result.avatarUrl) {
setCurrentAvatarUrl(result.avatarUrl);
setSuccess(true);
setTimeout(() => setSuccess(false), 3000);
} else {
setError(result.error || "Failed to upload avatar");
}
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

The success state is reused for both profile updates and avatar actions, but the success banner text is always "Profile updated successfully!". With the new avatar upload/remove flows calling setSuccess(true), users will see a misleading message when only their avatar changed. Consider splitting this into separate success states/messages (e.g. profileSuccess vs avatarSuccess) or making the banner message reflect the last successful action.

Copilot uses AI. Check for mistakes.
Comment thread actions/account/avatar.ts Outdated
Comment on lines +10 to +14
export interface UploadAvatarResult {
success: boolean;
error?: string;
avatarUrl?: string;
}
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

UploadAvatarResult is also used as the return type of removeAvatar, which makes the API a bit confusing (the name implies only upload). Consider renaming to a more neutral type (e.g. AvatarActionResult) or defining distinct result types for upload vs remove.

Copilot uses AI. Check for mistakes.
@sirily11
Copy link
Copy Markdown
Contributor

sirily11 commented Mar 6, 2026

@copilot use vercel blob client side upload and limit to 4mb maximum. use onUploadCompleted callback behavior to callbackend to replace user uploaded image to webp. Download it first then compress and convert to webp and reupload to the storage. Remove the user uploaded one, and replaced by the newly uploaded version.

…ompleted

- Create /api/avatar/upload route with handleUpload for client-side uploads
- Use onUploadCompleted callback to download, convert to WebP via sharp, and re-upload
- Update profile form to use @vercel/blob/client upload() with polling for processed URL
- Add sharp dependency for server-side image processing
- Increase max file size to 4MB
- Fix removeAvatar ordering: clear DB first, then delete blob (safe ordering)
- Rename UploadAvatarResult to AvatarActionResult
- Split success messages for profile vs avatar actions
- Add getAvatarUrl server action for polling processed avatar URL
- Update tests for new architecture (7 tests passing)

Co-authored-by: sirily11 <32106111+sirily11@users.noreply.github.com>
- Convert handleRemoveAvatar to async function instead of IIFE
- Move polling delay to end of loop for immediate first check
- Remove unnecessary blob.url comparison in old avatar cleanup
- Fix double type cast with spread operator for allowedContentTypes

Co-authored-by: sirily11 <32106111+sirily11@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 6, 2026

@copilot use vercel blob client side upload and limit to 4mb maximum. use onUploadCompleted callback behavior t...

Implemented in commits 40b688a and 616aa5c:

  • Switched to Vercel Blob client-side upload using upload() from @vercel/blob/client with handleUploadUrl: "/api/avatar/upload"
  • Max file size increased to 4MB
  • Created /api/avatar/upload route with handleUpload that uses onUploadCompleted callback to:
    1. Download the uploaded image from blob storage
    2. Convert/compress to WebP using sharp (quality 80)
    3. Re-upload the WebP version
    4. Delete the original uploaded file
    5. Clean up any previous avatar
    6. Update the user's avatarUrl in DB with the new WebP URL
  • Client polls getAvatarUrl server action after upload to get the processed WebP URL
  • Added sharp as an explicit dependency for server-side image processing

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • vercel.com
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node /home/REDACTED/work/_temp/ghcca-node/node/bin/node --enable-source-maps /home/REDACTED/work/_temp/copilot-developer-action-main/dist/index.js (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI changed the title feat(profile): add avatar upload support using Vercel Blob feat(profile): add avatar upload support using Vercel Blob with client-side upload and WebP conversion Mar 6, 2026
@sirily11
Copy link
Copy Markdown
Contributor

sirily11 commented Mar 6, 2026

@copilot
CleanShot 2026-03-07 at 00 11 00@2x
the avatar in the header not using the avatar uploaded

…ticon

Include avatarUrl in the account layout DB query and prefer it over
the identicon fallback for the header avatar image.

Co-authored-by: sirily11 <32106111+sirily11@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 6, 2026

@copilot
CleanShot 2026-03-07 at 00 11 00@2x
the avatar ...

Fixed in 3faa417. The account layout was missing avatarUrl in the DB query and always using the identicon fallback. Now it queries avatarUrl and prefers it over the identicon for the header avatar:

src={user.avatarUrl || `/api/avatar/${user.avatarSeed || user.id}`}

@sirily11
Copy link
Copy Markdown
Contributor

sirily11 commented Mar 6, 2026

@copilot
CleanShot 2026-03-07 at 00 54 08@2x
Avatar in the header not in circle

…xed dimensions

Co-authored-by: sirily11 <32106111+sirily11@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 6, 2026

@copilot
CleanShot 2026-03-07 at 00 54 08@2x
Avatar in the header not in...

Fixed in 09b2076. Added object-cover w-8 h-8 to the header avatar Image so uploaded images are properly cropped and clipped to a circle.

@sirily11 sirily11 enabled auto-merge (squash) March 6, 2026 18:12
@sirily11 sirily11 merged commit dc3669a into main Mar 6, 2026
3 checks passed
@sirily11 sirily11 deleted the copilot/add-avatar-support-user branch March 6, 2026 18:18
Copilot AI added a commit that referenced this pull request Mar 7, 2026
…t-side upload and WebP conversion (#31)

* Initial plan

* feat: add avatar upload support using Vercel Blob

- Add avatarUrl column to users table schema
- Create database migration for avatarUrl column
- Create uploadAvatar and removeAvatar server actions
- Update profile form with avatar upload/remove UI
- Update OAuth userinfo and token endpoints to use avatarUrl
- Add avatar cleanup on user and account deletion
- Update next.config.ts with Vercel Blob image domain
- Add unit tests for avatar actions

Co-authored-by: sirily11 <32106111+sirily11@users.noreply.github.com>

* refactor: extract avatar constants to shared module

Co-authored-by: sirily11 <32106111+sirily11@users.noreply.github.com>

* feat: switch to client-side upload with WebP conversion via onUploadCompleted

- Create /api/avatar/upload route with handleUpload for client-side uploads
- Use onUploadCompleted callback to download, convert to WebP via sharp, and re-upload
- Update profile form to use @vercel/blob/client upload() with polling for processed URL
- Add sharp dependency for server-side image processing
- Increase max file size to 4MB
- Fix removeAvatar ordering: clear DB first, then delete blob (safe ordering)
- Rename UploadAvatarResult to AvatarActionResult
- Split success messages for profile vs avatar actions
- Add getAvatarUrl server action for polling processed avatar URL
- Update tests for new architecture (7 tests passing)

Co-authored-by: sirily11 <32106111+sirily11@users.noreply.github.com>

* fix: address code review feedback

- Convert handleRemoveAvatar to async function instead of IIFE
- Move polling delay to end of loop for immediate first check
- Remove unnecessary blob.url comparison in old avatar cleanup
- Fix double type cast with spread operator for allowedContentTypes

Co-authored-by: sirily11 <32106111+sirily11@users.noreply.github.com>

* fix: use uploaded avatar URL in header instead of always showing identicon

Include avatarUrl in the account layout DB query and prefer it over
the identicon fallback for the header avatar image.

Co-authored-by: sirily11 <32106111+sirily11@users.noreply.github.com>

* fix: ensure header avatar displays as circle with object-cover and fixed dimensions

Co-authored-by: sirily11 <32106111+sirily11@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sirily11 <32106111+sirily11@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants