feat(profile): add avatar upload support using Vercel Blob with client-side upload and WebP conversion#31
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
- 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>
There was a problem hiding this comment.
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_urlwith 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.
| // 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"); |
There was a problem hiding this comment.
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).
| // 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!)); |
There was a problem hiding this comment.
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.
| if (result.success && result.avatarUrl) { | ||
| setCurrentAvatarUrl(result.avatarUrl); | ||
| setSuccess(true); | ||
| setTimeout(() => setSuccess(false), 3000); | ||
| } else { | ||
| setError(result.error || "Failed to upload avatar"); | ||
| } |
There was a problem hiding this comment.
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.
| export interface UploadAvatarResult { | ||
| success: boolean; | ||
| error?: string; | ||
| avatarUrl?: string; | ||
| } |
There was a problem hiding this comment.
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 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>
Implemented in commits 40b688a and 616aa5c:
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:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
@copilot |
…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>
|
@copilot |
…xed dimensions Co-authored-by: sirily11 <32106111+sirily11@users.noreply.github.com>
…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>


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