第四章:資料庫對接:Supabase Adapter 綁定與 User Profile 資料庫擴充
經過前三章的努力,我們現在擁有了一個完美運行的 OAuth 2.0 雙平台登入系統。使用者可以透過 Google 或 Line 登入,而且完全不需要我們自己處理密碼加密。
但現在我們面臨一個巨大的問題:資料存哪裡了? 預設情況下,Auth.js 採用的是 JWT 模式 (JSON Web Token)。這代表當使用者登入成功後,Auth.js 會把使用者的姓名、Email、大頭貼網址,全部「加密」後塞進使用者的瀏覽器 Cookie 裡面。
這對於簡單的部落格來說夠用了,但對於我們正在打造的 Vibe Tutor 這種 SaaS 知識付費平台來說,這是完全行不通的!
為什麼 SaaS 必須要有真正的資料庫?
試想以下情境:
- 你要怎麼知道網站目前有多少會員? (Cookie 是存在每個人的電腦裡,你根本算不出來)
- 小明買了 3,999 元的課程,你要怎麼紀錄他「已付款」? (如果你把已付款狀態寫在 JWT 裡,駭客只要篡改 JWT 的 payload,就能免費看課)
- 小明如果犯規被停權,你要怎麼踢他下線? (JWT 發出去後就無法在伺服器端強制註銷,除非等它過期)
所以,我們必須引入一個強大的關聯式資料庫,並且透過 Adapter 架構,讓 Auth.js 把每一次註冊與登入的資料,全都自動寫進我們的資料庫裡!
本章將使用地表最強的 Serverless 資料庫:Supabase (PostgreSQL) 來進行實戰。
🗄️ 步驟一:準備 Supabase 資料庫 Schema
要讓 Auth.js 看得懂你的資料庫,你的資料表 (Tables) 不能亂開,必須嚴格遵守 Auth.js 官方定義的 Schema 規範。
- 登入 Supabase 專案儀表板。
- 點擊左側的 SQL Editor,開一個新的 Query。
- 貼上並執行以下這段由 Auth.js 官方提供的 PostgreSQL 建表語法:
```sql -- 建立核心的 User 表 (儲存「人」的實體) CREATE TABLE users ( id uuid NOT NULL DEFAULT uuid_generate_v4(), name VARCHAR(255), email VARCHAR(255), "emailVerified" TIMESTAMPTZ, image TEXT,
-- 你可以在這裡大膽擴充你自己的商業欄位! membership_level VARCHAR(50) DEFAULT 'free', total_spent INT DEFAULT 0,
PRIMARY KEY (id) );
-- 建立 Accounts 關聯表 (儲存這個人綁定了哪些第三方 Provider) CREATE TABLE accounts ( id uuid NOT NULL DEFAULT uuid_generate_v4(), "userId" uuid NOT NULL, type VARCHAR(255) NOT NULL, provider VARCHAR(255) NOT NULL, "providerAccountId" VARCHAR(255) NOT NULL, refresh_token TEXT, access_token TEXT, expires_at BIGINT, token_type TEXT, scope TEXT, id_token TEXT, session_state TEXT, PRIMARY KEY (id), -- 設定外鍵,如果 User 被刪除,他底下的綁定帳號也會一併刪除 CONSTRAINT fk_user FOREIGN KEY ("userId") REFERENCES users(id) ON DELETE CASCADE );
-- 建立 Sessions 表 (儲存使用者目前在哪台裝置上登入中) CREATE TABLE sessions ( id uuid NOT NULL DEFAULT uuid_generate_v4(), "sessionToken" VARCHAR(255) NOT NULL, "userId" uuid NOT NULL, expires TIMESTAMPTZ NOT NULL, PRIMARY KEY (id), CONSTRAINT fk_user FOREIGN KEY ("userId") REFERENCES users(id) ON DELETE CASCADE );
-- 為了效能,建立必要的索引 (Indexes) CREATE UNIQUE INDEX accounts_provider_provideraccountid_idx ON accounts (provider, "providerAccountId"); CREATE UNIQUE INDEX sessions_sessiontoken_idx ON sessions ("sessionToken"); CREATE UNIQUE INDEX users_email_idx ON users (email); ```
這段 SQL 語法價值連城! 仔細看,我們在 `users` 表裡面偷加了兩個欄位:`membership_level` (會員等級) 和 `total_spent` (總消費額)。這就是 Database Session 架構最迷人的地方:你可以無限擴充與商業邏輯相關的資料欄位。
🔌 步驟二:安裝 Supabase Adapter
回到你的專案終端機,安裝 Auth.js 為 Supabase 量身打造的 Adapter 套件,以及 Supabase 的官方 SDK:
```bash npm install @auth/supabase-adapter @supabase/supabase-js ```
安裝完成後,請確保你的 `.env.local` 中有設定 Supabase 的連線字串。 🚨 極度重要的資安警告: 由於 Auth.js 是在伺服器背景替我們「偷偷寫入」使用者資料,它必須繞過 Supabase 的所有 RLS 安全防護網。所以這裡我們必須使用 Service Role Key (超級管理員金鑰),絕對不能使用 `anon key`。
```env
.env.local 加上這兩個
NEXT_PUBLIC_SUPABASE_URL="https://xxxxx.supabase.co"
注意!這把金鑰絕對不能有 NEXT_PUBLIC_ 前綴,否則會外洩到前端!
SUPABASE_SERVICE_ROLE_KEY="eyJhbGciOiJIUz...這是一大串超級金鑰" ```
⚙️ 步驟三:在 auth.ts 啟動 Adapter 引擎
現在,我們要對 `src/auth.ts` 進行大幅度的改寫。把原本存在虛無縹緲的 Cookie 模式,強制切換成硬派的 Database 模式。
打開 `src/auth.ts`:
```typescript import NextAuth from "next-auth" import GoogleProvider from "next-auth/providers/google" import LineProvider from "next-auth/providers/line" import { SupabaseAdapter } from "@auth/supabase-adapter"
export const { handlers, signIn, signOut, auth } = NextAuth({ providers: [ GoogleProvider({ clientId: process.env.AUTH_GOOGLE_ID, clientSecret: process.env.AUTH_GOOGLE_SECRET, }), LineProvider({ clientId: process.env.AUTH_LINE_ID, clientSecret: process.env.AUTH_LINE_SECRET, }), ],
// 🚀 關鍵設定一:掛載 Adapter adapter: SupabaseAdapter({ url: process.env.NEXT_PUBLIC_SUPABASE_URL!, secret: process.env.SUPABASE_SERVICE_ROLE_KEY!, }),
// 🚀 關鍵設定二:強制切換 Session 策略 session: { // 當你設定了 Adapter,預設其實就是 "database"。 // 但寫清楚 "database" 有助於團隊理解架構。 strategy: "database", // 設定登入多久後會被踢下線 (這裡設為 30 天) maxAge: 30 * 24 * 60 * 60, },
// 🚀 關鍵設定三:Callbacks (攔截器) callbacks: { // 當你在前端呼叫 auth() 或 useSession() 時,這個 callback 會被觸發 // 我們可以趁機把資料庫裡的額外欄位 (如 membership_level) 塞給前端! async session({ session, user }) { if (session.user) { // user 物件是直接從資料庫 select 出來的真實資料! session.user.id = user.id; // 如果你有擴充型別定義,你甚至可以這樣做: // session.user.membership_level = user.membership_level; } return session; } } }) ```
🎇 步驟四:見證奇蹟的時刻
你已經設定好了一切。現在是收割成果的時候了。
- 確保開發伺服器運行中 (`npm run dev`)。
- 前往 `http://localhost:3000/api/auth/signin`。
- 用你從沒登入過的另一個 Google 帳號,點擊登入。
- 登入成功後,請立刻前往 Supabase 的後台 Dashboard,點開 Table Editor。
準備迎接震撼教育:
- 點開 `users` 表:你看到了剛剛登入的名字、Email、和高清的大頭貼網址!你沒有寫任何一句 `INSERT INTO users` 的 SQL 語法!
- 點開 `accounts` 表:你看到了一筆關聯資料,記錄了這個使用者是用 `google` 登入的,並且把 Google 給的 `access_token` 完美的存下來了。
- 點開 `sessions` 表:你看到了一組隨機的 `sessionToken` 和過期時間。
這就是架構之美。 如果這時候,你以管理員的身分,手動在 Supabase 後台把 `sessions` 表裡的那筆紀錄刪除。 幾秒鐘後,該名使用者在瀏覽器上的狀態就會瞬間變成「未登入」。這就是為什麼大型商業系統一定要用 Database Session 的原因,你掌握了絕對的控制權!
下一章,我們將來到本門課的最高潮:如何利用這個強大的 Auth.js,在伺服器端攔截那些沒有付費的「白嫖仔」,打造滴水不漏的商業級資安防護網!