iT邦幫忙

2025 iThome 鐵人賽

DAY 23
0
Software Development

我獨自開發 - 用 Supabase 打造全端應用系列 第 23

第二十三關 - 來企排隊:Supabase 快速建立上傳個人頭像

  • 分享至 

  • xImage
  •  

封面

今天透過 Supabase Storage ,快速實作使用者的頭像上傳功能。

技術實作

第一步:建立檔案儲存空間

在開始之前,需要在 Supabase 中設定檔案儲存功能,建立 Storage Bucket。

在 Supabase Dashboard > Storage > New Bucket 中建立名為 avatars 的儲存桶:

-- supabase/migrations/20250101000000_setup_storage.sql

-- 建立頭像儲存桶
INSERT INTO storage.buckets (id, name, public)
VALUES ('avatars', 'avatars', true);

-- 設定儲存桶的存取政策
CREATE POLICY "Avatar images are publicly accessible" ON storage.objects
  FOR SELECT USING (bucket_id = 'avatars');

CREATE POLICY "Users can upload their own avatar" ON storage.objects
  FOR INSERT WITH CHECK (
    bucket_id = 'avatars'
    AND auth.uid()::text = (storage.foldername(name))[1]
  );

CREATE POLICY "Users can update their own avatar" ON storage.objects
  FOR UPDATE USING (
    bucket_id = 'avatars'
    AND auth.uid()::text = (storage.foldername(name))[1]
  );

第二步:建立頭像上傳服務

新增上傳頭像函式

// lib/supabase/users-client.ts

export const userProfileService = {
  // 上傳頭像圖片
  async uploadAvatar(file: File): Promise<string | null> {
    const supabase = createClient();
    const {
      data: { user },
    } = await supabase.auth.getUser();

    if (!user) throw new Error("使用者未登入");

    // 取得檔案副檔名
    const fileExt = file.name.split(".").pop();
    const fileName = `${user.id}.${fileExt}`;
    const filePath = `${user.id}/${fileName}`;

    // 上傳檔案到 Supabase Storage
    const { error: uploadError } = await supabase.storage
      .from("avatars")
      .upload(filePath, file, { upsert: true });

    if (uploadError) {
      console.error("上傳頭像失敗:", uploadError);
      throw uploadError;
    }

    // 取得公開存取網址
    const { data: publicUrlData } = supabase.storage
      .from("avatars")
      .getPublicUrl(filePath);

    return publicUrlData.publicUrl;
  },
};

程式碼說明

  1. 身份驗證檢查

    const {
      data: { user },
    } = await supabase.auth.getUser();
    if (!user) throw new Error("使用者未登入");
    

    確保只有登入的使用者才能上傳頭像。

  2. 檔案路徑

    const fileExt = file.name.split(".").pop();
    const fileName = `${user.id}.${fileExt}`;
    const filePath = `${user.id}/${fileName}`;
    
    • 每個使用者都有專屬的資料夾(以使用者 ID 命名)
    • 檔案名稱使用使用者 ID,確保唯一性
    • 保留原始檔案的副檔名
  3. 檔案上傳

    const { error: uploadError } = await supabase.storage
      .from("avatars")
      .upload(filePath, file, { upsert: true });
    
    • upsert: true 表示如果檔案已存在就覆蓋,這樣使用者更換頭像時不會產生重複檔案
  4. 取得公開網址

    const { data: publicUrlData } = supabase.storage
      .from("avatars")
      .getPublicUrl(filePath);
    

    取得可以在網頁上顯示的公開網址。

第三步:建立前端上傳介面

// components/user-profile-form.tsx

export function UserProfileForm() {
  // 處理頭像上傳
  const handleAvatarChange = async (
    event: React.ChangeEvent<HTMLInputElement>
  ) => {
    const file = event.target.files[0];

    try {
      // 上傳頭像到 Supabase Storage
      const publicUrl = await userProfileService.uploadAvatar(file);

      if (publicUrl) {
        // 更新使用者資料中的頭像網址
        const updatedProfile = await userProfileService.updateProfile({
          avatar_url: publicUrl,
        });

        if (updatedProfile) {
          setProfile(updatedProfile);
          toast.success("頭像上傳成功!");
        }
      }
    } catch (error) {
      console.error("上傳頭像失敗:", error);
      toast.error("上傳頭像失敗,請稍後再試。");
    }
  };
}

測試與驗證

第一步:準備測試環境

  1. 啟動開發伺服器

    npm run dev
    
  2. 確認 Supabase 連線

    • 檢查 .env.local 中的 Supabase 設定
    • 確認 Storage 功能已啟用

第二步:功能測試

2.1 基本上傳測試

  1. 前往個人資料頁面
  2. 選擇一張圖片檔案
  3. 檢查頭像是否立即更新

2.2 檔案格式測試

測試不同的圖片格式:

  • ✅ JPG 檔案
  • ✅ PNG 檔案
  • ✅ WebP 檔案
  • ❌ 非圖片檔案(應該被拒絕)

2.3 錯誤情況測試

  1. 未登入狀態:確認會顯示適當的錯誤訊息
  2. 大檔案上傳:測試檔案大小限制

第三步:檢查資料庫

在 Supabase Dashboard 中驗證。

  1. Storage 檢查

    • 前往 Storage > avatars
    • 確認檔案已正確上傳到使用者專屬資料夾
  2. 使用者資料檢查

    • 前往 Table Editor > users
    • 確認 avatar_url 欄位已更新為正確的公開網址

小結

透過這個頭像上傳功能,使用者能夠建立更完整的個人檔案。

... to be continued

有任何想討論歡迎留言,或需要指正的地方請鞭大力一點,歡迎訂閱、按讚加分享,分享給想要提升開發效率的朋友


上一篇
第二十二關 - 來企排隊:Supabase 快速建立信箱註冊與登入
系列文
我獨自開發 - 用 Supabase 打造全端應用23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言