Connect personal information card on the profile page to the database#42
Conversation
|
@legend4tech is attempting to deploy a commit to the Trustless Work Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughAdds end-to-end avatar upload (UI, AuthService.uploadAvatar, Supabase avatars bucket + migration), refactors profile page user-data mapping and save/cancel flows with explicit loading/error handling, normalizes email handling and quote styles across configs, and small UI/typing adjustments. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant ProfileInfo as ProfileInfo Component
participant AuthService as AuthService
participant SupabaseStorage as Supabase Storage
participant Database as Users Table
User->>ProfileInfo: Select image file
ProfileInfo->>ProfileInfo: Validate file (type image/*, <=5MB)
ProfileInfo->>AuthService: uploadAvatar(userId, file)
AuthService->>SupabaseStorage: Delete old avatars (cleanup), Upload new file
SupabaseStorage-->>AuthService: Return public URL
AuthService->>Database: Update user's avatar_url
Database-->>AuthService: Confirm update
AuthService-->>ProfileInfo: Return avatar URL
ProfileInfo->>ProfileInfo: onUserDataChange(updated avatar_url)
ProfileInfo->>User: Show new avatar and toast
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Fix all issues with AI agents
In `@apps/web/app/dashboard/profile/page.tsx`:
- Around line 43-79: The returned UserData object overwrites saved preferences
by hardcoding notifications, security, and payment_methods; update the return to
prefer baseUser values (baseUser?.notifications, baseUser?.security,
baseUser?.payment_methods) with sensible defaults as fallbacks (e.g.,
email_trades/email_escrows/push true, sms false;
two_factor_enabled/login_notifications default;
sinpe_number/preferred_method/bank_accounts defaults) so existing DB preferences
are preserved when present while keeping the current defaults when those fields
are missing.
In `@apps/web/components/profile/ProfileInfo.tsx`:
- Around line 77-101: The frontend validates image uploads too loosely in
handleAvatarUpload by only checking file.type.startsWith("image/"), which allows
SVG/TIFF etc. — change the validation to check the file MIME against the exact
allowed list (e.g.,
"image/jpeg","image/jpg","image/png","image/gif","image/webp") and show a
specific toast like "Please select a jpeg, png, gif, or webp image" when it
fails; ensure handleAvatarUpload returns early after showing the toast and that
uploading state is properly reset on all early exits and errors so the UI
doesn't hang.
- Around line 80-86: The uploading state is set too early causing a flash when
the user cancels; update the file input change handler in ProfileInfo.tsx so
setUploading(true) is called only after validating event.target.files exists and
the chosen file passes any validations (i.e., move setUploading(true) below the
if (!event.target.files || event.target.files.length === 0) check and after
file-type/size checks). Keep the existing try/catch/finally that calls
setUploading(false) so that any early returns do not leave the state
stuck—ensure any early-return branches occur before setUploading(true) to avoid
a false "Uploading…" flash.
In `@apps/web/lib/services/auth.ts`:
- Around line 225-258: The uploadAvatar function can produce "undefined" file
extensions when file.name has no dot; change the fileExt logic to default to a
safe extension (e.g., "png" or "bin") when file.name.split(".").pop() is falsy,
then build fileName and filePath using that fallback so filenames like
`${userId}-${timestamp}.${fileExt}` never contain "undefined"; update the
variable fileExt (and thus fileName/filePath) in uploadAvatar accordingly and
ensure any sanitization (trim/toLowerCase) is applied to the derived extension.
In `@supabase/migrations/20260205000000_add_avatars_storage_bucket.sql`:
- Around line 78-107: The function cleanup_old_user_avatars is SECURITY DEFINER
and currently allows any authenticated caller to delete another user's avatars;
fix it by adding an ownership/authorization check at the top of the function
body that compares the requested user_uuid to the caller's authenticated user id
(e.g., compare user_uuid to the JWT subject via
current_setting('request.jwt.claim.sub')::uuid or use auth.uid() if available),
and if they don't match either RAISE EXCEPTION 'not authorized' or RETURN 0
before performing the DELETE; keep the check inside cleanup_old_user_avatars and
keep GRANT EXECUTE only for authenticated users.
🧹 Nitpick comments (4)
apps/web/next.config.ts (1)
19-25: Dev-only remote pattern is always included in production builds.The
127.0.0.1:54321pattern is harmless in production (it won't resolve to anything useful), but you could gate it behind aNODE_ENV === "development"check to keep the production config minimal. Not blocking.♻️ Optional: conditionally include dev pattern
remotePatterns: [ { protocol: "https", hostname: "images.unsplash.com" }, { protocol: "https", hostname: "static.wikia.nocookie.net" }, { protocol: "https", hostname: (() => { const url = process.env.NEXT_PUBLIC_SUPABASE_URL; try { return url ? new URL(url).hostname : "localhost"; } catch { return "localhost"; } })(), }, - - // local Supabase development server - { - protocol: "http", - hostname: "127.0.0.1", - port: "54321", - }, + // local Supabase development server + ...(process.env.NODE_ENV === "development" + ? [{ protocol: "http" as const, hostname: "127.0.0.1", port: "54321" }] + : []), ],apps/web/lib/services/auth.ts (1)
252-257: Old avatars accumulate — the DB cleanup function is never invoked.Each upload creates a new file (
upsert: false), butcleanup_old_user_avatarsfrom the migration is never called. Over time, storage will grow unbounded per user. Consider calling the cleanup function after a successful upload, or scheduling it periodically.♻️ Optional: invoke cleanup after upload
// Update user profile with new avatar URL in database await this.updateUserProfile(userId, { avatar_url: publicUrl, }); + // Clean up old avatars, keeping only the 3 most recent + await supabase.rpc("cleanup_old_user_avatars", { + user_uuid: userId, + keep_latest: 3, + }); + return publicUrl;apps/web/components/profile/ProfileInfo.tsx (1)
29-33: Function declaration instead of const arrow function.Guidelines prefer const arrow functions with explicit type annotations. This is a minor consistency nit.
♻️ Optional: convert to arrow function
-export function ProfileInfo({ +export const ProfileInfo = ({ userData, isEditing, onUserDataChange, -}: ProfileInfoProps) { +}: ProfileInfoProps) => {As per coding guidelines, "Prefer const arrow functions with explicit type annotations over function declarations."
apps/web/app/dashboard/profile/page.tsx (1)
93-110:kyc_statusis included in the save payload — consider excluding it.While the UI doesn't expose KYC as an editable field, the payload still sends
kyc_statustoupdateUserProfile. If RLS doesn't restrict this column, a user could manipulate state to self-verify. Safer to omit it from the client-side payload and control it server-side only.🔒 Proposed fix: exclude kyc_status from payload
const payload = { ...(hydratedUserData.email && !hydratedUserData.email.endsWith("@wallet.local") ? { email: hydratedUserData.email } : {}), full_name: hydratedUserData.full_name, username: hydratedUserData.username, bio: hydratedUserData.bio, avatar_url: hydratedUserData.avatar_url, phone: hydratedUserData.phone, country: hydratedUserData.country, - kyc_status: hydratedUserData.kyc_status, notifications: hydratedUserData.notifications, security: hydratedUserData.security, payment_methods: hydratedUserData.payment_methods, stellar_address: hydratedUserData.stellar_address, } as const;
| return { | ||
| id: baseUser?.id || localOverrides?.id || '', | ||
| id: baseUser?.id || "", | ||
| email: normalizedEmail, | ||
| full_name: baseUser?.full_name || localOverrides?.full_name || '', | ||
| username: baseUser?.username || localOverrides?.username || '', | ||
| bio: baseUser?.bio || localOverrides?.bio || '', | ||
| avatar_url: baseUser?.avatar_url || localOverrides?.avatar_url || '', | ||
| stellar_address: | ||
| baseUser?.stellar_address || localOverrides?.stellar_address || '', | ||
| phone: baseUser?.phone || localOverrides?.phone || '', | ||
| country: baseUser?.country || localOverrides?.country || '', | ||
| kyc_status: | ||
| baseUser?.kyc_status || localOverrides?.kyc_status || 'pending', | ||
| reputation_score: | ||
| baseUser?.reputation_score ?? localOverrides?.reputation_score ?? 0, | ||
| total_trades: | ||
| baseUser?.total_trades ?? localOverrides?.total_trades ?? 0, | ||
| total_volume: | ||
| baseUser?.total_volume ?? localOverrides?.total_volume ?? 0, | ||
| created_at: | ||
| baseUser?.created_at || | ||
| localOverrides?.created_at || | ||
| new Date().toISOString(), | ||
| notifications: localOverrides?.notifications ?? { | ||
| full_name: baseUser?.full_name || "", | ||
| username: baseUser?.username || "", | ||
| bio: baseUser?.bio || "", | ||
| avatar_url: baseUser?.avatar_url || "", | ||
| stellar_address: baseUser?.stellar_address || "", | ||
| phone: baseUser?.phone || "", | ||
| country: baseUser?.country || "", | ||
| kyc_status: baseUser?.kyc_status || "pending", | ||
| reputation_score: baseUser?.reputation_score ?? 0, | ||
| total_trades: baseUser?.total_trades ?? 0, | ||
| total_volume: baseUser?.total_volume ?? 0, | ||
| created_at: baseUser?.created_at || new Date().toISOString(), | ||
| notifications: { | ||
| email_trades: true, | ||
| email_escrows: true, | ||
| push_notifications: true, | ||
| sms_notifications: false, | ||
| }, | ||
| security: localOverrides?.security ?? { | ||
| security: { | ||
| two_factor_enabled: true, | ||
| login_notifications: true, | ||
| }, | ||
| payment_methods: localOverrides?.payment_methods ?? { | ||
| sinpe_number: '', | ||
| preferred_method: 'sinpe', | ||
| payment_methods: { | ||
| sinpe_number: "", | ||
| preferred_method: "sinpe", | ||
| bank_accounts: [ | ||
| { | ||
| bank_iban: '', | ||
| bank_name: '', | ||
| bank_account_holder: '', | ||
| bank_iban: "", | ||
| bank_name: "", | ||
| bank_account_holder: "", | ||
| }, | ||
| ], | ||
| }, | ||
| }; |
There was a problem hiding this comment.
Saved notification/security/payment preferences from the database are silently discarded.
When building UserData from baseUser (Lines 43–79), the notifications, security, and payment_methods fields are hardcoded with defaults instead of reading from baseUser?.notifications, baseUser?.security, and baseUser?.payment_methods. Any preferences the user previously saved are overwritten on every page load.
🐛 Proposed fix: prefer DB values, fall back to defaults
notifications: {
- email_trades: true,
- email_escrows: true,
- push_notifications: true,
- sms_notifications: false,
+ email_trades: baseUser?.notifications?.email_trades ?? true,
+ email_escrows: baseUser?.notifications?.email_escrows ?? true,
+ push_notifications: baseUser?.notifications?.push_notifications ?? true,
+ sms_notifications: baseUser?.notifications?.sms_notifications ?? false,
},
security: {
- two_factor_enabled: true,
- login_notifications: true,
+ two_factor_enabled: baseUser?.security?.two_factor_enabled ?? false,
+ login_notifications: baseUser?.security?.login_notifications ?? true,
},
payment_methods: {
- sinpe_number: "",
- preferred_method: "sinpe",
+ sinpe_number: baseUser?.payment_methods?.sinpe_number ?? "",
+ preferred_method: baseUser?.payment_methods?.preferred_method ?? "sinpe",
bank_accounts: [
{
- bank_iban: "",
- bank_name: "",
- bank_account_holder: "",
+ bank_iban: baseUser?.payment_methods?.bank_iban ?? "",
+ bank_name: baseUser?.payment_methods?.bank_name ?? "",
+ bank_account_holder: baseUser?.payment_methods?.bank_account_holder ?? "",
},
],
},🤖 Prompt for AI Agents
In `@apps/web/app/dashboard/profile/page.tsx` around lines 43 - 79, The returned
UserData object overwrites saved preferences by hardcoding notifications,
security, and payment_methods; update the return to prefer baseUser values
(baseUser?.notifications, baseUser?.security, baseUser?.payment_methods) with
sensible defaults as fallbacks (e.g., email_trades/email_escrows/push true, sms
false; two_factor_enabled/login_notifications default;
sinpe_number/preferred_method/bank_accounts defaults) so existing DB preferences
are preserved when present while keeping the current defaults when those fields
are missing.
| const handleAvatarUpload = async ( | ||
| event: React.ChangeEvent<HTMLInputElement>, | ||
| ) => { | ||
| try { | ||
| setUploading(true); | ||
|
|
||
| // Check if file was selected | ||
| if (!event.target.files || event.target.files.length === 0) { | ||
| return; | ||
| } | ||
|
|
||
| const file = event.target.files[0]; | ||
|
|
||
| // Validate file type | ||
| if (!file.type.startsWith("image/")) { | ||
| toast.error("Please select an image file"); | ||
| return; | ||
| } | ||
|
|
||
| // Validate file size (max 5MB) | ||
| const maxSize = 5 * 1024 * 1024; // 5MB in bytes | ||
| if (file.size > maxSize) { | ||
| toast.error("Image size must be less than 5MB"); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Frontend type validation is looser than the server-side allowed MIME types.
file.type.startsWith("image/") accepts types like image/svg+xml, image/tiff, etc., but the storage bucket only allows jpeg, jpg, png, gif, webp. If a user picks an SVG, the frontend passes validation but the upload fails with a storage error, producing a generic "Failed to upload avatar" toast rather than a clear validation message.
🛡️ Proposed fix: match frontend validation to bucket MIME types
+ const allowedTypes = [
+ "image/jpeg",
+ "image/jpg",
+ "image/png",
+ "image/gif",
+ "image/webp",
+ ];
// Validate file type
- if (!file.type.startsWith("image/")) {
- toast.error("Please select an image file");
+ if (!allowedTypes.includes(file.type)) {
+ toast.error("Please select a JPEG, PNG, GIF, or WebP image");
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.
| const handleAvatarUpload = async ( | |
| event: React.ChangeEvent<HTMLInputElement>, | |
| ) => { | |
| try { | |
| setUploading(true); | |
| // Check if file was selected | |
| if (!event.target.files || event.target.files.length === 0) { | |
| return; | |
| } | |
| const file = event.target.files[0]; | |
| // Validate file type | |
| if (!file.type.startsWith("image/")) { | |
| toast.error("Please select an image file"); | |
| return; | |
| } | |
| // Validate file size (max 5MB) | |
| const maxSize = 5 * 1024 * 1024; // 5MB in bytes | |
| if (file.size > maxSize) { | |
| toast.error("Image size must be less than 5MB"); | |
| return; | |
| } | |
| const handleAvatarUpload = async ( | |
| event: React.ChangeEvent<HTMLInputElement>, | |
| ) => { | |
| try { | |
| setUploading(true); | |
| // Check if file was selected | |
| if (!event.target.files || event.target.files.length === 0) { | |
| return; | |
| } | |
| const file = event.target.files[0]; | |
| const allowedTypes = [ | |
| "image/jpeg", | |
| "image/jpg", | |
| "image/png", | |
| "image/gif", | |
| "image/webp", | |
| ]; | |
| // Validate file type | |
| if (!allowedTypes.includes(file.type)) { | |
| toast.error("Please select a JPEG, PNG, GIF, or WebP image"); | |
| return; | |
| } | |
| // Validate file size (max 5MB) | |
| const maxSize = 5 * 1024 * 1024; // 5MB in bytes | |
| if (file.size > maxSize) { | |
| toast.error("Image size must be less than 5MB"); | |
| return; | |
| } |
🤖 Prompt for AI Agents
In `@apps/web/components/profile/ProfileInfo.tsx` around lines 77 - 101, The
frontend validates image uploads too loosely in handleAvatarUpload by only
checking file.type.startsWith("image/"), which allows SVG/TIFF etc. — change the
validation to check the file MIME against the exact allowed list (e.g.,
"image/jpeg","image/jpg","image/png","image/gif","image/webp") and show a
specific toast like "Please select a jpeg, png, gif, or webp image" when it
fails; ensure handleAvatarUpload returns early after showing the toast and that
uploading state is properly reset on all early exits and errors so the UI
doesn't hang.
| try { | ||
| setUploading(true); | ||
|
|
||
| // Check if file was selected | ||
| if (!event.target.files || event.target.files.length === 0) { | ||
| return; | ||
| } |
There was a problem hiding this comment.
setUploading(true) fires before validation — brief "Uploading…" flash on cancel/invalid file.
If the user opens the file picker and cancels (or picks an invalid file), uploading is set to true on Line 81 then reset in finally. This causes a brief flash of the "Uploading…" text on the button.
🛡️ Proposed fix: move state change after validation
const handleAvatarUpload = async (
event: React.ChangeEvent<HTMLInputElement>,
) => {
- try {
- setUploading(true);
-
// Check if file was selected
if (!event.target.files || event.target.files.length === 0) {
return;
}
const file = event.target.files[0];
// Validate file type
if (!file.type.startsWith("image/")) {
toast.error("Please select an image file");
return;
}
// Validate file size (max 5MB)
const maxSize = 5 * 1024 * 1024;
if (file.size > maxSize) {
toast.error("Image size must be less than 5MB");
return;
}
+ try {
+ setUploading(true);
+
// Upload to Supabase Storage and save to database🤖 Prompt for AI Agents
In `@apps/web/components/profile/ProfileInfo.tsx` around lines 80 - 86, The
uploading state is set too early causing a flash when the user cancels; update
the file input change handler in ProfileInfo.tsx so setUploading(true) is called
only after validating event.target.files exists and the chosen file passes any
validations (i.e., move setUploading(true) below the if (!event.target.files ||
event.target.files.length === 0) check and after file-type/size checks). Keep
the existing try/catch/finally that calls setUploading(false) so that any early
returns do not leave the state stuck—ensure any early-return branches occur
before setUploading(true) to avoid a false "Uploading…" flash.
| static async uploadAvatar(userId: string, file: File): Promise<string> { | ||
| // Generate unique filename | ||
| const fileExt = file.name.split(".").pop(); | ||
| const timestamp = Date.now(); | ||
| const fileName = `${userId}-${timestamp}.${fileExt}`; | ||
| const filePath = `${userId}/${fileName}`; | ||
|
|
||
| // Upload file to Supabase Storage bucket 'avatars' | ||
| const { error: uploadError } = await supabase.storage | ||
| .from("avatars") | ||
| .upload(filePath, file, { | ||
| cacheControl: "3600", | ||
| upsert: false, // Don't overwrite, create new file each time | ||
| }); | ||
|
|
||
| if (uploadError) { | ||
| console.error("Avatar upload error:", uploadError); | ||
| throw uploadError; | ||
| } | ||
|
|
||
| // Get public URL of uploaded file | ||
| const { data: urlData } = supabase.storage | ||
| .from("avatars") | ||
| .getPublicUrl(filePath); | ||
|
|
||
| const publicUrl = urlData.publicUrl; | ||
|
|
||
| // Update user profile with new avatar URL in database | ||
| await this.updateUserProfile(userId, { | ||
| avatar_url: publicUrl, | ||
| }); | ||
|
|
||
| return publicUrl; | ||
| } |
There was a problem hiding this comment.
uploadAvatar implementation is clean — one edge case on file extension.
file.name.split(".").pop() returns undefined for files without an extension (e.g., "avatar"), producing a path like userId/userId-123.undefined. The bucket's MIME type check will still protect against invalid files, but the stored filename would be misleading.
🛡️ Proposed fix: default extension fallback
- const fileExt = file.name.split(".").pop();
+ const fileExt = file.name.split(".").pop() || "jpg";🤖 Prompt for AI Agents
In `@apps/web/lib/services/auth.ts` around lines 225 - 258, The uploadAvatar
function can produce "undefined" file extensions when file.name has no dot;
change the fileExt logic to default to a safe extension (e.g., "png" or "bin")
when file.name.split(".").pop() is falsy, then build fileName and filePath using
that fallback so filenames like `${userId}-${timestamp}.${fileExt}` never
contain "undefined"; update the variable fileExt (and thus fileName/filePath) in
uploadAvatar accordingly and ensure any sanitization (trim/toLowerCase) is
applied to the derived extension.
supabase/migrations/20260205000000_add_avatars_storage_bucket.sql
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@apps/web/lib/services/auth.ts`:
- Around line 232-258: The uploaded avatar can become orphaned if
this.updateUserProfile fails and previous avatars are never removed; before
uploading, call the existing cleanup_old_user_avatars() (or fetch the user's
current avatar and delete it) to remove prior files under the user's folder,
then perform the upload; wrap the call to this.updateUserProfile(...) in a
try/catch and on failure remove the newly uploaded blob from the "avatars"
bucket (using the same storage remove mechanism) to roll back the upload and
rethrow the error so callers still see the failure.
🧹 Nitpick comments (1)
apps/web/lib/services/auth.ts (1)
225-258: No server-side file type/size validation before uploading.The frontend (
ProfileInfo.tsx) validates file type and size, butuploadAvataritself performs no checks. Any caller (or a future caller) can pass an arbitraryFiledirectly. Adding a guard here hardens the service layer independently of the UI.🛡️ Suggested validation
static async uploadAvatar(userId: string, file: File): Promise<string> { + const MAX_SIZE = 5 * 1024 * 1024; // 5 MB + const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"]; + + if (!ALLOWED_TYPES.includes(file.type)) { + throw new Error("Invalid file type. Only JPEG, PNG, GIF, and WebP are allowed."); + } + if (file.size > MAX_SIZE) { + throw new Error("File size exceeds 5 MB limit."); + } + // Generate unique filename const fileExt = file.name.split(".").pop();
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@apps/web/lib/services/auth.ts`:
- Around line 232-253: The current flow deletes existing avatar files before
uploading the new one, which can leave a broken avatar if the upload or DB
update fails; change the sequence in the avatar upload routine so you first
upload the new file via supabase.storage.from("avatars").upload (check
uploadError), then update the user's avatar_url in the DB, and only after both
upload and DB update succeed remove the old files (use the existing
existingFiles/filesToRemove logic with supabase.storage.from("avatars").remove).
Ensure you still set upsert/cacheControl as before and throw/log uploadError or
DB errors without deleting previous files.
🧹 Nitpick comments (1)
apps/web/lib/services/auth.ts (1)
237-240: Silently ignoring errors fromremove()could mask storage issues.The result of
supabase.storage.from("avatars").remove(filesToRemove)is not checked. If the removal fails (e.g., permissions, network), you get no signal. At minimum, log the error so storage-cleanup failures are visible in diagnostics.- await supabase.storage.from("avatars").remove(filesToRemove); + const { error: removeError } = await supabase.storage.from("avatars").remove(filesToRemove); + if (removeError) { + console.warn("Failed to remove old avatars:", removeError); + }
apps/web/lib/services/auth.ts
Outdated
There was a problem hiding this comment.
Deleting old avatars before the new upload succeeds can leave the user with a broken avatar.
If list + remove (lines 233–240) succeeds but upload (line 243) then fails, the old files are gone while the DB still holds a stale avatar_url pointing to the deleted object. The user sees a broken image with no way to recover except uploading again.
Move the old-file cleanup to after a successful upload (or even after the successful DB update) so the previous avatar remains intact if anything goes wrong.
♻️ Suggested reorder: delete old files after successful upload + DB update
static async uploadAvatar(userId: string, file: File): Promise<string> {
const fileExt = file.name.split(".").pop() || "jpg";
const timestamp = Date.now();
const fileName = `${userId}-${timestamp}.${fileExt}`;
const filePath = `${userId}/${fileName}`;
- // Delete ALL old avatars for this user BEFORE uploading
- const { data: existingFiles } = await supabase.storage
- .from("avatars")
- .list(userId);
-
- if (existingFiles && existingFiles.length > 0) {
- const filesToRemove = existingFiles.map((f) => `${userId}/${f.name}`);
- await supabase.storage.from("avatars").remove(filesToRemove);
- }
-
// Upload new avatar file
const { error: uploadError } = await supabase.storage
.from("avatars")
.upload(filePath, file, {
cacheControl: "3600",
upsert: false,
});
if (uploadError) {
console.error("Avatar upload error:", uploadError);
throw uploadError;
}
const { data: urlData } = supabase.storage
.from("avatars")
.getPublicUrl(filePath);
const publicUrl = urlData.publicUrl;
try {
await this.updateUserProfile(userId, { avatar_url: publicUrl });
} catch (error) {
await supabase.storage.from("avatars").remove([filePath]);
throw error;
}
+ // Clean up old avatars only after everything succeeded
+ const { data: existingFiles } = await supabase.storage
+ .from("avatars")
+ .list(userId);
+
+ if (existingFiles && existingFiles.length > 0) {
+ const filesToRemove = existingFiles
+ .filter((f) => f.name !== fileName)
+ .map((f) => `${userId}/${f.name}`);
+ if (filesToRemove.length > 0) {
+ await supabase.storage.from("avatars").remove(filesToRemove);
+ }
+ }
+
return publicUrl;
}🤖 Prompt for AI Agents
In `@apps/web/lib/services/auth.ts` around lines 232 - 253, The current flow
deletes existing avatar files before uploading the new one, which can leave a
broken avatar if the upload or DB update fails; change the sequence in the
avatar upload routine so you first upload the new file via
supabase.storage.from("avatars").upload (check uploadError), then update the
user's avatar_url in the DB, and only after both upload and DB update succeed
remove the old files (use the existing existingFiles/filesToRemove logic with
supabase.storage.from("avatars").remove). Ensure you still set
upsert/cacheControl as before and throw/log uploadError or DB errors without
deleting previous files.
|
GM Gm @aguilar1x still waiting for review |
Pull Request: Connect Personal Information Card to Database with Avatar Upload
📋 Overview
This PR establishes a complete database connection for the Personal Information card on the profile page, enabling users to view, edit, and persist their profile data. Additionally, it implements avatar upload functionality using Supabase Storage.
Closes: #31
🎯 Problem Statement
The Personal Information card was previously displaying static/disconnected data:
✅ Solution
Implemented a complete data flow from database → UI → database with the following improvements:
1. Database Layer
2. Backend Services
uploadAvatar()function toAuthServicefor handling image uploadsgetUserProfile()correctly fetches all user dataupdateUserProfile()saves changes with proper timestamp updates3. State Management
useAuthhook to properly manage user profile state4. UI Components
📝 Changes Made
Migration Files
supabase/migrations/20260205000000_add_avatars_storage_bucket.sqlPurpose: Creates Supabase Storage infrastructure for avatar uploads
What it does:
avatarsbucket with 5MB file size limit{user_id}/)cleanup_old_user_avatars()helper function to manage storage spaceKey Features:
ON CONFLICTfor bucket creationDROP POLICY IF EXISTSfor policy recreationBackend Files
apps/web/lib/services/auth.tsPurpose: Central service for all authentication and user profile operations
Changes Made:
Added
uploadAvatar()static methodWhat it does:
{userId}-{timestamp}.{extension}avatarsavatars/{userId}/{userId}-{timestamp}.{ext}updateUserProfile()Flow:
No other changes - Existing functions (
getUserProfile,updateUserProfile) were already correct.Hook Files
apps/web/hooks/use-auth.tsPurpose: React hook providing authentication state and profile management
Changes Made:
updateProfile()correctly callsAuthService.updateUserProfile()Verified functionality:
Page Components
apps/web/app/dashboard/profile/page.tsxPurpose: Main profile page managing edit mode and save operations
Changes Made:
Fixed
mapUserToUserData()function (lines 27-87)localOverridesexists, return it completely (prevents stale data)baseUserwhen no local edits existImproved
handleSave()function (lines 95-124)setIsEditing(false)inside try block for better UXcatchtocatch (error)to access error detailsconsole.errorfor debuggingsetUserData(null)after save (allows re-editing)Before:
After:
Added
handleCancel()function (lines 126-130)userDatatonullto discard unsaved changesUpdated Cancel button (line 167)
onClick={() => setIsEditing(false)}onClick={handleCancel}disabled={isLoading}to prevent clicks during saveWhy these changes?
UI Components
apps/web/components/profile/ProfileInfo.tsxPurpose: Displays personal information form with avatar upload
Changes Made:
Added imports (lines 1-2)
Added upload state (line 27)
Added
getAvatarFallback()function (lines 56-68)Before (crashed on empty name):
After (safe):
Added
handleAvatarUpload()function (lines 70-117)AuthService.uploadAvatar()Flow:
Replaced avatar button with file upload (lines 145-163)
<input type="file">with idavatar-upload<label>that triggers file input when clickedBefore:
After:
Why these changes?
Configuration Files
next.config.tsPurpose: Next.js configuration for image optimization and external resources
Changes Made:
Added Supabase Storage domain to image configuration:
Why this change?
<Image>component to optimize avatar images from local Supabasehttp://127.0.0.1:54321/storage/...would be blocked🔄 Data Flow
Viewing Profile
Editing Profile
Saving Changes
Uploading Avatar
Canceling Edits
🧪 Testing Performed
Manual Testing
✅ Profile Loading
✅ Edit Mode
✅ Saving Changes
✅ Canceling Changes
✅ Avatar Upload
✅ Error Handling
✅ Edge Cases
Database Verification
✅ Migration Applied Successfully
✅ Policies Created
✅ Data Persists
✅ Files Uploaded
📸Screen Recording
New_Recording_-_2_6_2026._1_04_33_AM.mp4
🔐 Security Considerations
Row Level Security (RLS)
Authentication
auth.uid()in RLS policiesValidation
📦 File Structure After Changes
✅ Checklist
Summary by CodeRabbit
New Features
Bug Fixes
Chores