Overview

Accept FormData, upload a new file to Supabase Storage when provided, keep or clear the existing avatar accordingly, and update profiles.

import { createAdminClient } from '@/config/supabase/adminClient'
import { DatabaseTable } from '@/constants/enums'
import { getCurrentUser } from '@/lib/auth'
import { uploadAvatar } from '@/lib/storage'
import type { ActionResponse } from '@/types'

export async function updateUserProfileAction(formData: FormData): Promise<ActionResponse> {
  const supabase = createAdminClient()
  const user = await getCurrentUser()
  if (!user) return { success: false, error: 'Not authenticated' }

  const name = String(formData.get('name') ?? '')
  const file = formData.get('avatarFile') as File | null
  const existing = String(formData.get('existingAvatar') ?? '')
  const removed = existing === ''

  let avatarUrl: string | null = null
  if (file) avatarUrl = await uploadAvatar(file, user.id)
  else if (!removed) avatarUrl = existing || null

  const update: Record<string, string | null> = { full_name: name }
  if (file || removed) update.avatar_url = avatarUrl

  const { error } = await supabase
    .from(DatabaseTable.Profiles)
    .update(update)
    .eq('id', user.id)

  if (error) return { success: false, error: 'Failed to update profile' }
  return { success: true }
}

Key conventions

  • Three states for the avatar field: new file uploaded, existing URL kept, or explicitly removed (empty string sent from the form).
  • Upload happens in uploadAvatar(file, userId) which returns the public URL from the avatars Supabase Storage bucket.
  • Only update avatar_url in the database when the avatar actually changed — avoids unnecessary writes when only full_name changed.
  • Use the admin client for the profiles update so RLS doesn't block server-side writes.