[{"data":1,"prerenderedAt":2302},["ShallowReactive",2],{"blog:\u002Fblog\u002Fnuxt\u002Fbetter-auth-integration":3},{"id":4,"title":5,"author":6,"body":7,"category":2286,"date":2287,"description":2288,"draft":2289,"extension":2290,"image":2291,"meta":2292,"navigation":344,"path":2293,"seo":2294,"series":2295,"seriesOrder":259,"seriesTitle":2296,"stem":2297,"tags":2298,"updatedAt":2291,"__hash__":2301},"blog\u002Fblog\u002Fnuxt\u002Fbetter-auth-integration\u002Findex.md","nuxt-better-auth 認證整合","charles",{"type":8,"value":9,"toc":2246},"minimark",[10,14,18,42,45,49,55,133,150,152,155,165,167,170,174,197,201,299,303,564,568,648,650,654,658,680,684,762,766,796,800,835,838,853,855,859,862,976,979,1145,1149,1287,1289,1293,1297,1431,1434,1506,1508,1511,1515,1653,1657,1817,1819,1822,1874,1876,1879,1883,1889,1895,1901,1988,1992,1997,2002,2016,2020,2025,2033,2095,2097,2100,2164,2166,2169,2205,2207,2210,2242],[11,12,13],"h2",{"id":13},"這篇要解決什麼問題",[15,16,17],"p",{},"認證是每個應用程式的核心功能。這篇文章將說明：",[19,20,21,30,36,39],"ul",{},[22,23,24,25,29],"li",{},"為什麼捨棄 ",[26,27,28],"code",{},"@nuxtjs\u002Fsupabase"," 的內建 Auth",[22,31,32,35],{},[26,33,34],{},"@onmax\u002Fnuxt-better-auth"," 的完整用法",[22,37,38],{},"OAuth 社交登入實作（Google）",[22,40,41],{},"Client 與 Server 端的 Session 管理",[43,44],"hr",{},[11,46,48],{"id":47},"為什麼不用-supabase-auth","為什麼不用 Supabase Auth？",[15,50,51,52,54],{},"Supabase 確實提供內建的 Auth 功能，但我們選擇 ",[26,53,34],{}," 的原因：",[56,57,58,74],"table",{},[59,60,61],"thead",{},[62,63,64,68,71],"tr",{},[65,66,67],"th",{},"比較項目",[65,69,70],{},"Supabase Auth",[65,72,73],{},"nuxt-better-auth",[75,76,77,89,100,111,122],"tbody",{},[62,78,79,83,86],{},[80,81,82],"td",{},"Session 管理",[80,84,85],{},"自己的 Session 機制",[80,87,88],{},"標準 HTTP-only Cookie",[62,90,91,94,97],{},[80,92,93],{},"與 Nuxt 整合",[80,95,96],{},"需額外設定",[80,98,99],{},"原生支援",[62,101,102,105,108],{},[80,103,104],{},"自訂使用者欄位",[80,106,107],{},"受限於 auth.users",[80,109,110],{},"完全自訂",[62,112,113,116,119],{},[80,114,115],{},"多 Provider 支援",[80,117,118],{},"良好",[80,120,121],{},"優秀",[62,123,124,127,130],{},[80,125,126],{},"角色權限",[80,128,129],{},"需自行實作",[80,131,132],{},"內建支援",[15,134,135,139,140,142,143,146,147,149],{},[136,137,138],"strong",{},"關鍵決策","：我們使用 ",[26,141,28],{}," ",[136,144,145],{},"僅作為資料庫存取","，認證完全交給 ",[26,148,34],{},"。",[43,151],{},[11,153,154],{"id":154},"認證架構概覽",[156,157,162],"pre",{"className":158,"code":160,"language":161},[159],"language-text","┌─────────────────────────────────────────────────────────────────┐\n│                        Client (Browser)                          │\n├─────────────────────────────────────────────────────────────────┤\n│  const { user, loggedIn, signIn, signOut } = useUserSession()    │\n│  await signIn.social({ provider: 'google' })                     │\n│  await signIn.email({ email, password })                         │\n└─────────────────────────────────────────────────────────────────┘\n                              ↓ HTTP-only Cookie\n┌─────────────────────────────────────────────────────────────────┐\n│                         Server (Nitro)                           │\n├─────────────────────────────────────────────────────────────────┤\n│  const { user } = await requireUserSession(event)                │\n│  const { user } = await requireUserSession(event, {              │\n│    user: { role: 'admin' }                                       │\n│  })                                                              │\n└─────────────────────────────────────────────────────────────────┘\n                              ↓ Service Role Key\n┌─────────────────────────────────────────────────────────────────┐\n│                      Supabase (Database)                         │\n├─────────────────────────────────────────────────────────────────┤\n│  使用 Service Role Client 執行資料庫操作                          │\n│  RLS 保護讀取操作                                                 │\n└─────────────────────────────────────────────────────────────────┘\n","text",[26,163,160],{"__ignoreMap":164},"",[43,166],{},[11,168,169],{"id":169},"安裝與設定",[171,172,173],"h3",{"id":173},"安裝套件",[156,175,179],{"className":176,"code":177,"language":178,"meta":164,"style":164},"language-bash shiki shiki-themes material-theme-lighter github-light github-dark","pnpm add @onmax\u002Fnuxt-better-auth\n","bash",[26,180,181],{"__ignoreMap":164},[182,183,186,190,194],"span",{"class":184,"line":185},"line",1,[182,187,189],{"class":188},"sbgvK","pnpm",[182,191,193],{"class":192},"s_sjI"," add",[182,195,196],{"class":192}," @onmax\u002Fnuxt-better-auth\n",[171,198,200],{"id":199},"nuxtconfigts","nuxt.config.ts",[156,202,206],{"className":203,"code":204,"language":205,"meta":164,"style":164},"language-typescript shiki shiki-themes material-theme-lighter github-light github-dark","export default defineNuxtConfig({\n  modules: [\n    \"@onmax\u002Fnuxt-better-auth\",\n    \"@nuxtjs\u002Fsupabase\", \u002F\u002F 僅作資料庫存取\n    \u002F\u002F ...\n  ],\n});\n","typescript",[26,207,208,229,242,257,273,279,287],{"__ignoreMap":164},[182,209,210,214,217,221,225],{"class":184,"line":185},[182,211,213],{"class":212},"sVHd0","export",[182,215,216],{"class":212}," default",[182,218,220],{"class":219},"sGLFI"," defineNuxtConfig",[182,222,224],{"class":223},"su5hD","(",[182,226,228],{"class":227},"sP7_E","{\n",[182,230,232,236,239],{"class":184,"line":231},2,[182,233,235],{"class":234},"skxfh","  modules",[182,237,238],{"class":227},":",[182,240,241],{"class":223}," [\n",[182,243,245,249,251,254],{"class":184,"line":244},3,[182,246,248],{"class":247},"sjJ54","    \"",[182,250,34],{"class":192},[182,252,253],{"class":247},"\"",[182,255,256],{"class":227},",\n",[182,258,260,262,264,266,269],{"class":184,"line":259},4,[182,261,248],{"class":247},[182,263,28],{"class":192},[182,265,253],{"class":247},[182,267,268],{"class":227},",",[182,270,272],{"class":271},"sutJx"," \u002F\u002F 僅作資料庫存取\n",[182,274,276],{"class":184,"line":275},5,[182,277,278],{"class":271},"    \u002F\u002F ...\n",[182,280,282,285],{"class":184,"line":281},6,[182,283,284],{"class":223},"  ]",[182,286,256],{"class":227},[182,288,290,293,296],{"class":184,"line":289},7,[182,291,292],{"class":227},"}",[182,294,295],{"class":223},")",[182,297,298],{"class":227},";\n",[171,300,302],{"id":301},"server-端設定","Server 端設定",[156,304,306],{"className":203,"code":305,"language":205,"meta":164,"style":164},"\u002F\u002F server\u002Fauth.config.ts\nimport { defineServerAuth } from \"@onmax\u002Fnuxt-better-auth\u002Fconfig\";\n\nexport default defineServerAuth({\n  \u002F\u002F 啟用 Email + Password 認證（開發測試用）\n  emailAndPassword: { enabled: true },\n\n  \u002F\u002F OAuth providers\n  socialProviders: {\n    google: {\n      clientId: process.env.NUXT_OAUTH_GOOGLE_CLIENT_ID,\n      clientSecret: process.env.NUXT_OAUTH_GOOGLE_CLIENT_SECRET,\n    },\n  },\n\n  \u002F\u002F Session 設定\n  session: {\n    expiresIn: 60 * 60 * 24 * 7, \u002F\u002F 7 天\n    updateAge: 60 * 60 * 24, \u002F\u002F 每 24 小時更新\n  },\n});\n",[26,307,308,313,340,346,358,363,384,388,394,405,415,440,461,467,473,478,484,494,527,550,555],{"__ignoreMap":164},[182,309,310],{"class":184,"line":185},[182,311,312],{"class":271},"\u002F\u002F server\u002Fauth.config.ts\n",[182,314,315,318,321,324,327,330,333,336,338],{"class":184,"line":231},[182,316,317],{"class":212},"import",[182,319,320],{"class":227}," {",[182,322,323],{"class":223}," defineServerAuth",[182,325,326],{"class":227}," }",[182,328,329],{"class":212}," from",[182,331,332],{"class":247}," \"",[182,334,335],{"class":192},"@onmax\u002Fnuxt-better-auth\u002Fconfig",[182,337,253],{"class":247},[182,339,298],{"class":227},[182,341,342],{"class":184,"line":244},[182,343,345],{"emptyLinePlaceholder":344},true,"\n",[182,347,348,350,352,354,356],{"class":184,"line":259},[182,349,213],{"class":212},[182,351,216],{"class":212},[182,353,323],{"class":219},[182,355,224],{"class":223},[182,357,228],{"class":227},[182,359,360],{"class":184,"line":275},[182,361,362],{"class":271},"  \u002F\u002F 啟用 Email + Password 認證（開發測試用）\n",[182,364,365,368,370,372,375,377,381],{"class":184,"line":281},[182,366,367],{"class":234},"  emailAndPassword",[182,369,238],{"class":227},[182,371,320],{"class":227},[182,373,374],{"class":234}," enabled",[182,376,238],{"class":227},[182,378,380],{"class":379},"syTEX"," true",[182,382,383],{"class":227}," },\n",[182,385,386],{"class":184,"line":289},[182,387,345],{"emptyLinePlaceholder":344},[182,389,391],{"class":184,"line":390},8,[182,392,393],{"class":271},"  \u002F\u002F OAuth providers\n",[182,395,397,400,402],{"class":184,"line":396},9,[182,398,399],{"class":234},"  socialProviders",[182,401,238],{"class":227},[182,403,404],{"class":227}," {\n",[182,406,408,411,413],{"class":184,"line":407},10,[182,409,410],{"class":234},"    google",[182,412,238],{"class":227},[182,414,404],{"class":227},[182,416,418,421,423,426,429,432,434,438],{"class":184,"line":417},11,[182,419,420],{"class":234},"      clientId",[182,422,238],{"class":227},[182,424,425],{"class":223}," process",[182,427,428],{"class":227},".",[182,430,431],{"class":223},"env",[182,433,428],{"class":227},[182,435,437],{"class":436},"s_hVV","NUXT_OAUTH_GOOGLE_CLIENT_ID",[182,439,256],{"class":227},[182,441,443,446,448,450,452,454,456,459],{"class":184,"line":442},12,[182,444,445],{"class":234},"      clientSecret",[182,447,238],{"class":227},[182,449,425],{"class":223},[182,451,428],{"class":227},[182,453,431],{"class":223},[182,455,428],{"class":227},[182,457,458],{"class":436},"NUXT_OAUTH_GOOGLE_CLIENT_SECRET",[182,460,256],{"class":227},[182,462,464],{"class":184,"line":463},13,[182,465,466],{"class":227},"    },\n",[182,468,470],{"class":184,"line":469},14,[182,471,472],{"class":227},"  },\n",[182,474,476],{"class":184,"line":475},15,[182,477,345],{"emptyLinePlaceholder":344},[182,479,481],{"class":184,"line":480},16,[182,482,483],{"class":271},"  \u002F\u002F Session 設定\n",[182,485,487,490,492],{"class":184,"line":486},17,[182,488,489],{"class":234},"  session",[182,491,238],{"class":227},[182,493,404],{"class":227},[182,495,497,500,502,506,510,512,514,517,519,522,524],{"class":184,"line":496},18,[182,498,499],{"class":234},"    expiresIn",[182,501,238],{"class":227},[182,503,505],{"class":504},"srdBf"," 60",[182,507,509],{"class":508},"smGrS"," *",[182,511,505],{"class":504},[182,513,509],{"class":508},[182,515,516],{"class":504}," 24",[182,518,509],{"class":508},[182,520,521],{"class":504}," 7",[182,523,268],{"class":227},[182,525,526],{"class":271}," \u002F\u002F 7 天\n",[182,528,530,533,535,537,539,541,543,545,547],{"class":184,"line":529},19,[182,531,532],{"class":234},"    updateAge",[182,534,238],{"class":227},[182,536,505],{"class":504},[182,538,509],{"class":508},[182,540,505],{"class":504},[182,542,509],{"class":508},[182,544,516],{"class":504},[182,546,268],{"class":227},[182,548,549],{"class":271}," \u002F\u002F 每 24 小時更新\n",[182,551,553],{"class":184,"line":552},20,[182,554,472],{"class":227},[182,556,558,560,562],{"class":184,"line":557},21,[182,559,292],{"class":227},[182,561,295],{"class":223},[182,563,298],{"class":227},[171,565,567],{"id":566},"client-端設定","Client 端設定",[156,569,571],{"className":203,"code":570,"language":205,"meta":164,"style":164},"\u002F\u002F app\u002Fauth.config.ts\nimport { defineClientAuth } from \"@onmax\u002Fnuxt-better-auth\u002Fconfig\";\n\nexport default defineClientAuth({\n  \u002F\u002F 可在此加入 client-side plugins\n  \u002F\u002F plugins: [\n  \u002F\u002F   twoFactorClient(),\n  \u002F\u002F   passkeyClient(),\n  \u002F\u002F ]\n});\n",[26,572,573,578,599,603,615,620,625,630,635,640],{"__ignoreMap":164},[182,574,575],{"class":184,"line":185},[182,576,577],{"class":271},"\u002F\u002F app\u002Fauth.config.ts\n",[182,579,580,582,584,587,589,591,593,595,597],{"class":184,"line":231},[182,581,317],{"class":212},[182,583,320],{"class":227},[182,585,586],{"class":223}," defineClientAuth",[182,588,326],{"class":227},[182,590,329],{"class":212},[182,592,332],{"class":247},[182,594,335],{"class":192},[182,596,253],{"class":247},[182,598,298],{"class":227},[182,600,601],{"class":184,"line":244},[182,602,345],{"emptyLinePlaceholder":344},[182,604,605,607,609,611,613],{"class":184,"line":259},[182,606,213],{"class":212},[182,608,216],{"class":212},[182,610,586],{"class":219},[182,612,224],{"class":223},[182,614,228],{"class":227},[182,616,617],{"class":184,"line":275},[182,618,619],{"class":271},"  \u002F\u002F 可在此加入 client-side plugins\n",[182,621,622],{"class":184,"line":281},[182,623,624],{"class":271},"  \u002F\u002F plugins: [\n",[182,626,627],{"class":184,"line":289},[182,628,629],{"class":271},"  \u002F\u002F   twoFactorClient(),\n",[182,631,632],{"class":184,"line":390},[182,633,634],{"class":271},"  \u002F\u002F   passkeyClient(),\n",[182,636,637],{"class":184,"line":396},[182,638,639],{"class":271},"  \u002F\u002F ]\n",[182,641,642,644,646],{"class":184,"line":407},[182,643,292],{"class":227},[182,645,295],{"class":223},[182,647,298],{"class":227},[43,649],{},[11,651,653],{"id":652},"client-端認證","Client 端認證",[171,655,657],{"id":656},"useusersession-核心用法","useUserSession 核心用法",[156,659,663],{"className":660,"code":661,"language":662,"meta":164,"style":164},"language-vue shiki shiki-themes material-theme-lighter github-light github-dark","&lt;script setup lang=\"ts\"&gt; const { user, loggedIn, signIn, signOut, fetch }\n= useUserSession() \u002F\u002F 檢查登入狀態 if (loggedIn.value) {\nconsole.log('使用者已登入:', user.value) } &lt;\u002Fscript&gt;\n","vue",[26,664,665,670,675],{"__ignoreMap":164},[182,666,667],{"class":184,"line":185},[182,668,669],{"class":223},"&lt;script setup lang=\"ts\"&gt; const { user, loggedIn, signIn, signOut, fetch }\n",[182,671,672],{"class":184,"line":231},[182,673,674],{"class":223},"= useUserSession() \u002F\u002F 檢查登入狀態 if (loggedIn.value) {\n",[182,676,677],{"class":184,"line":244},[182,678,679],{"class":223},"console.log('使用者已登入:', user.value) } &lt;\u002Fscript&gt;\n",[171,681,683],{"id":682},"api-說明","API 說明",[156,685,687],{"className":203,"code":686,"language":205,"meta":164,"style":164},"const {\n  user, \u002F\u002F Ref&lt;User | null&gt; - 當前使用者資料\n  loggedIn, \u002F\u002F ComputedRef&lt;boolean&gt; - 是否已登入\n  signIn, \u002F\u002F { social, email, ... } - 登入方法\n  signOut, \u002F\u002F () =&gt; Promise&lt;void&gt; - 登出\n  fetch, \u002F\u002F () =&gt; Promise&lt;void&gt; - 重新取得 session\n} = useUserSession();\n",[26,688,689,697,707,717,727,737,747],{"__ignoreMap":164},[182,690,691,695],{"class":184,"line":185},[182,692,694],{"class":693},"sbsja","const",[182,696,404],{"class":227},[182,698,699,702,704],{"class":184,"line":231},[182,700,701],{"class":436},"  user",[182,703,268],{"class":227},[182,705,706],{"class":271}," \u002F\u002F Ref&lt;User | null&gt; - 當前使用者資料\n",[182,708,709,712,714],{"class":184,"line":244},[182,710,711],{"class":436},"  loggedIn",[182,713,268],{"class":227},[182,715,716],{"class":271}," \u002F\u002F ComputedRef&lt;boolean&gt; - 是否已登入\n",[182,718,719,722,724],{"class":184,"line":259},[182,720,721],{"class":436},"  signIn",[182,723,268],{"class":227},[182,725,726],{"class":271}," \u002F\u002F { social, email, ... } - 登入方法\n",[182,728,729,732,734],{"class":184,"line":275},[182,730,731],{"class":436},"  signOut",[182,733,268],{"class":227},[182,735,736],{"class":271}," \u002F\u002F () =&gt; Promise&lt;void&gt; - 登出\n",[182,738,739,742,744],{"class":184,"line":281},[182,740,741],{"class":436},"  fetch",[182,743,268],{"class":227},[182,745,746],{"class":271}," \u002F\u002F () =&gt; Promise&lt;void&gt; - 重新取得 session\n",[182,748,749,751,754,757,760],{"class":184,"line":289},[182,750,292],{"class":227},[182,752,753],{"class":508}," =",[182,755,756],{"class":219}," useUserSession",[182,758,759],{"class":223},"()",[182,761,298],{"class":227},[171,763,765],{"id":764},"oauth-社交登入","OAuth 社交登入",[156,767,769],{"className":660,"code":768,"language":662,"meta":164,"style":164},"&lt;template&gt; &lt;UButton @click=\"loginWithGoogle\" icon=\"i-lucide-chrome\"&gt;\n使用 Google 登入 &lt;\u002FUButton&gt; &lt;\u002Ftemplate&gt; &lt;script setup\nlang=\"ts\"&gt; const { signIn } = useUserSession() async function\nloginWithGoogle() { await signIn.social({ provider: 'google' }) \u002F\u002F\n登入成功後會自動導向 callback URL } &lt;\u002Fscript&gt;\n",[26,770,771,776,781,786,791],{"__ignoreMap":164},[182,772,773],{"class":184,"line":185},[182,774,775],{"class":223},"&lt;template&gt; &lt;UButton @click=\"loginWithGoogle\" icon=\"i-lucide-chrome\"&gt;\n",[182,777,778],{"class":184,"line":231},[182,779,780],{"class":223},"使用 Google 登入 &lt;\u002FUButton&gt; &lt;\u002Ftemplate&gt; &lt;script setup\n",[182,782,783],{"class":184,"line":244},[182,784,785],{"class":223},"lang=\"ts\"&gt; const { signIn } = useUserSession() async function\n",[182,787,788],{"class":184,"line":259},[182,789,790],{"class":223},"loginWithGoogle() { await signIn.social({ provider: 'google' }) \u002F\u002F\n",[182,792,793],{"class":184,"line":275},[182,794,795],{"class":223},"登入成功後會自動導向 callback URL } &lt;\u002Fscript&gt;\n",[171,797,799],{"id":798},"emailpassword-登入","Email\u002FPassword 登入",[156,801,803],{"className":660,"code":802,"language":662,"meta":164,"style":164},"&lt;script setup lang=\"ts\"&gt; const { signIn } = useUserSession() const toast =\nuseToast() const email = ref('') const password = ref('') async function\nhandleLogin() { try { await signIn.email( { email: email.value, password:\npassword.value }, { onSuccess: () =&gt; navigateTo('\u002F') } ) } catch (error) {\ntoast.add({ title: '登入失敗', description: '請檢查帳號密碼是否正確', color:\n'red', }) } } &lt;\u002Fscript&gt;\n",[26,804,805,810,815,820,825,830],{"__ignoreMap":164},[182,806,807],{"class":184,"line":185},[182,808,809],{"class":223},"&lt;script setup lang=\"ts\"&gt; const { signIn } = useUserSession() const toast =\n",[182,811,812],{"class":184,"line":231},[182,813,814],{"class":223},"useToast() const email = ref('') const password = ref('') async function\n",[182,816,817],{"class":184,"line":244},[182,818,819],{"class":223},"handleLogin() { try { await signIn.email( { email: email.value, password:\n",[182,821,822],{"class":184,"line":259},[182,823,824],{"class":223},"password.value }, { onSuccess: () =&gt; navigateTo('\u002F') } ) } catch (error) {\n",[182,826,827],{"class":184,"line":275},[182,828,829],{"class":223},"toast.add({ title: '登入失敗', description: '請檢查帳號密碼是否正確', color:\n",[182,831,832],{"class":184,"line":281},[182,833,834],{"class":223},"'red', }) } } &lt;\u002Fscript&gt;\n",[171,836,837],{"id":837},"登出",[156,839,841],{"className":660,"code":840,"language":662,"meta":164,"style":164},"&lt;script setup lang=\"ts\"&gt; const { signOut } = useUserSession() async\nfunction handleLogout() { await signOut() navigateTo('\u002Flogin') } &lt;\u002Fscript&gt;\n",[26,842,843,848],{"__ignoreMap":164},[182,844,845],{"class":184,"line":185},[182,846,847],{"class":223},"&lt;script setup lang=\"ts\"&gt; const { signOut } = useUserSession() async\n",[182,849,850],{"class":184,"line":231},[182,851,852],{"class":223},"function handleLogout() { await signOut() navigateTo('\u002Flogin') } &lt;\u002Fscript&gt;\n",[43,854],{},[11,856,858],{"id":857},"server-端認證","Server 端認證",[171,860,861],{"id":861},"要求登入",[156,863,865],{"className":203,"code":864,"language":205,"meta":164,"style":164},"\u002F\u002F server\u002Fapi\u002Fv1\u002Fprofile.get.ts\nexport default defineEventHandler(async (event) =&gt; {\n  const { user } = await requireUserSession(event)\n  \u002F\u002F user 保證存在，否則會回傳 401\n\n  return { message: `Hello, ${user.name}` }\n})\n",[26,866,867,872,897,926,931,935,970],{"__ignoreMap":164},[182,868,869],{"class":184,"line":185},[182,870,871],{"class":271},"\u002F\u002F server\u002Fapi\u002Fv1\u002Fprofile.get.ts\n",[182,873,874,876,878,881,883,886,889,892,895],{"class":184,"line":231},[182,875,213],{"class":212},[182,877,216],{"class":212},[182,879,880],{"class":219}," defineEventHandler",[182,882,224],{"class":223},[182,884,885],{"class":219},"async",[182,887,888],{"class":223}," (event) ",[182,890,891],{"class":508},"=&",[182,893,894],{"class":223},"gt; ",[182,896,228],{"class":227},[182,898,899,902,905,908,910,913,917,919,923],{"class":184,"line":244},[182,900,901],{"class":223},"  const ",[182,903,904],{"class":227},"{",[182,906,907],{"class":223}," user",[182,909,326],{"class":227},[182,911,912],{"class":223}," = await ",[182,914,916],{"class":915},"sVXei","requireUserSession",[182,918,224],{"class":227},[182,920,922],{"class":921},"s99_P","event",[182,924,925],{"class":227},")\n",[182,927,928],{"class":184,"line":259},[182,929,930],{"class":271},"  \u002F\u002F user 保證存在，否則會回傳 401\n",[182,932,933],{"class":184,"line":275},[182,934,345],{"emptyLinePlaceholder":344},[182,936,937,940,942,945,947,950,953,956,959,961,964,967],{"class":184,"line":281},[182,938,939],{"class":223},"  return ",[182,941,904],{"class":227},[182,943,944],{"class":188}," message",[182,946,238],{"class":227},[182,948,949],{"class":247}," `",[182,951,952],{"class":192},"Hello, ",[182,954,955],{"class":247},"${",[182,957,958],{"class":223},"user",[182,960,428],{"class":247},[182,962,963],{"class":223},"name",[182,965,966],{"class":247},"}`",[182,968,969],{"class":227}," }\n",[182,971,972,974],{"class":184,"line":289},[182,973,292],{"class":227},[182,975,925],{"class":223},[171,977,978],{"id":978},"要求特定角色",[156,980,982],{"className":203,"code":981,"language":205,"meta":164,"style":164},"\u002F\u002F server\u002Fapi\u002Fadmin\u002Fusers.get.ts\nexport default defineEventHandler(async (event) =&gt; {\n  const { user } = await requireUserSession(event, {\n    user: { role: ['admin', 'manager'] },\n  })\n  \u002F\u002F 只有 admin 或 manager 可存取\n\n  const supabase = getServerSupabaseClient()\n  const { data } = await supabase.from('users').select('*')\n\n  return { data }\n})\n",[26,983,984,989,1009,1031,1072,1077,1082,1086,1097,1131,1135,1140],{"__ignoreMap":164},[182,985,986],{"class":184,"line":185},[182,987,988],{"class":271},"\u002F\u002F server\u002Fapi\u002Fadmin\u002Fusers.get.ts\n",[182,990,991,993,995,997,999,1001,1003,1005,1007],{"class":184,"line":231},[182,992,213],{"class":212},[182,994,216],{"class":212},[182,996,880],{"class":219},[182,998,224],{"class":223},[182,1000,885],{"class":219},[182,1002,888],{"class":223},[182,1004,891],{"class":508},[182,1006,894],{"class":223},[182,1008,228],{"class":227},[182,1010,1011,1013,1015,1017,1019,1021,1023,1025,1027,1029],{"class":184,"line":244},[182,1012,901],{"class":223},[182,1014,904],{"class":227},[182,1016,907],{"class":223},[182,1018,326],{"class":227},[182,1020,912],{"class":223},[182,1022,916],{"class":915},[182,1024,224],{"class":227},[182,1026,922],{"class":921},[182,1028,268],{"class":227},[182,1030,404],{"class":227},[182,1032,1033,1037,1039,1041,1044,1046,1049,1052,1055,1057,1059,1062,1065,1067,1070],{"class":184,"line":259},[182,1034,1036],{"class":1035},"sucvu","    user",[182,1038,238],{"class":227},[182,1040,320],{"class":227},[182,1042,1043],{"class":1035}," role",[182,1045,238],{"class":227},[182,1047,1048],{"class":227}," [",[182,1050,1051],{"class":247},"'",[182,1053,1054],{"class":192},"admin",[182,1056,1051],{"class":247},[182,1058,268],{"class":227},[182,1060,1061],{"class":247}," '",[182,1063,1064],{"class":192},"manager",[182,1066,1051],{"class":247},[182,1068,1069],{"class":227},"]",[182,1071,383],{"class":227},[182,1073,1074],{"class":184,"line":275},[182,1075,1076],{"class":227},"  })\n",[182,1078,1079],{"class":184,"line":281},[182,1080,1081],{"class":271},"  \u002F\u002F 只有 admin 或 manager 可存取\n",[182,1083,1084],{"class":184,"line":289},[182,1085,345],{"emptyLinePlaceholder":344},[182,1087,1088,1091,1094],{"class":184,"line":390},[182,1089,1090],{"class":223},"  const supabase = ",[182,1092,1093],{"class":915},"getServerSupabaseClient",[182,1095,1096],{"class":227},"()\n",[182,1098,1099,1101,1103,1106,1108,1111,1113,1117,1119,1122,1124,1127,1129],{"class":184,"line":396},[182,1100,901],{"class":223},[182,1102,904],{"class":227},[182,1104,1105],{"class":223}," data",[182,1107,326],{"class":227},[182,1109,1110],{"class":223}," = await supabase.from(",[182,1112,1051],{"class":247},[182,1114,1116],{"class":1115},"sZUrc","users",[182,1118,1051],{"class":247},[182,1120,1121],{"class":234},").select(",[182,1123,1051],{"class":247},[182,1125,1126],{"class":1115},"*",[182,1128,1051],{"class":247},[182,1130,925],{"class":234},[182,1132,1133],{"class":184,"line":407},[182,1134,345],{"emptyLinePlaceholder":344},[182,1136,1137],{"class":184,"line":417},[182,1138,1139],{"class":234},"  return { data }\n",[182,1141,1142],{"class":184,"line":442},[182,1143,1144],{"class":234},"})\n",[171,1146,1148],{"id":1147},"取得-session可能為-null","取得 Session（可能為 null）",[156,1150,1152],{"className":203,"code":1151,"language":205,"meta":164,"style":164},"\u002F\u002F 當你需要判斷是否登入，而非強制要求\nexport default defineEventHandler(async (event) =&gt; {\n  const session = await getUserSession(event)\n\n  if (session?.user) {\n    return { message: `歡迎回來，${session.user.name}` }\n  }\n\n  return { message: '請先登入' }\n})\n",[26,1153,1154,1159,1179,1196,1200,1221,1253,1258,1262,1281],{"__ignoreMap":164},[182,1155,1156],{"class":184,"line":185},[182,1157,1158],{"class":271},"\u002F\u002F 當你需要判斷是否登入，而非強制要求\n",[182,1160,1161,1163,1165,1167,1169,1171,1173,1175,1177],{"class":184,"line":231},[182,1162,213],{"class":212},[182,1164,216],{"class":212},[182,1166,880],{"class":219},[182,1168,224],{"class":223},[182,1170,885],{"class":219},[182,1172,888],{"class":223},[182,1174,891],{"class":508},[182,1176,894],{"class":223},[182,1178,228],{"class":227},[182,1180,1181,1184,1187,1190,1193],{"class":184,"line":244},[182,1182,1183],{"class":223},"  const session ",[182,1185,1186],{"class":508},"=",[182,1188,1189],{"class":212}," await",[182,1191,1192],{"class":219}," getUserSession",[182,1194,1195],{"class":223},"(event)\n",[182,1197,1198],{"class":184,"line":259},[182,1199,345],{"emptyLinePlaceholder":344},[182,1201,1202,1205,1208,1211,1214,1217,1219],{"class":184,"line":275},[182,1203,1204],{"class":915},"  if",[182,1206,1207],{"class":227}," (",[182,1209,1210],{"class":921},"session",[182,1212,1213],{"class":508},"?",[182,1215,1216],{"class":223},".user",[182,1218,295],{"class":227},[182,1220,404],{"class":227},[182,1222,1223,1226,1228,1230,1232,1234,1237,1239,1241,1243,1245,1247,1249,1251],{"class":184,"line":281},[182,1224,1225],{"class":212},"    return",[182,1227,320],{"class":227},[182,1229,944],{"class":234},[182,1231,238],{"class":227},[182,1233,949],{"class":247},[182,1235,1236],{"class":192},"歡迎回來，",[182,1238,955],{"class":247},[182,1240,1210],{"class":223},[182,1242,428],{"class":247},[182,1244,958],{"class":223},[182,1246,428],{"class":247},[182,1248,963],{"class":223},[182,1250,966],{"class":247},[182,1252,969],{"class":227},[182,1254,1255],{"class":184,"line":289},[182,1256,1257],{"class":227},"  }\n",[182,1259,1260],{"class":184,"line":390},[182,1261,345],{"emptyLinePlaceholder":344},[182,1263,1264,1266,1268,1270,1272,1274,1277,1279],{"class":184,"line":396},[182,1265,939],{"class":223},[182,1267,904],{"class":227},[182,1269,944],{"class":188},[182,1271,238],{"class":227},[182,1273,1061],{"class":247},[182,1275,1276],{"class":192},"請先登入",[182,1278,1051],{"class":247},[182,1280,969],{"class":227},[182,1282,1283,1285],{"class":184,"line":407},[182,1284,292],{"class":227},[182,1286,925],{"class":223},[43,1288],{},[11,1290,1292],{"id":1291},"session-型別定義","Session 型別定義",[171,1294,1296],{"id":1295},"擴充-user-型別","擴充 User 型別",[156,1298,1300],{"className":203,"code":1299,"language":205,"meta":164,"style":164},"\u002F\u002F server\u002Ftypes\u002Fauth.d.ts\ndeclare module \"@onmax\u002Fnuxt-better-auth\" {\n  interface User {\n    id: string;\n    email: string;\n    name?: string;\n    picture?: string;\n    role: \"admin\" | \"manager\" | \"staff\" | \"unauthorized\";\n  }\n}\n",[26,1301,1302,1307,1323,1333,1346,1357,1369,1380,1422,1426],{"__ignoreMap":164},[182,1303,1304],{"class":184,"line":185},[182,1305,1306],{"class":271},"\u002F\u002F server\u002Ftypes\u002Fauth.d.ts\n",[182,1308,1309,1312,1315,1317,1319,1321],{"class":184,"line":231},[182,1310,1311],{"class":693},"declare",[182,1313,1314],{"class":693}," module",[182,1316,332],{"class":247},[182,1318,34],{"class":192},[182,1320,253],{"class":247},[182,1322,404],{"class":227},[182,1324,1325,1328,1331],{"class":184,"line":244},[182,1326,1327],{"class":693},"  interface",[182,1329,1330],{"class":188}," User",[182,1332,404],{"class":227},[182,1334,1335,1338,1340,1344],{"class":184,"line":259},[182,1336,1337],{"class":1035},"    id",[182,1339,238],{"class":508},[182,1341,1343],{"class":1342},"sZMiF"," string",[182,1345,298],{"class":227},[182,1347,1348,1351,1353,1355],{"class":184,"line":275},[182,1349,1350],{"class":1035},"    email",[182,1352,238],{"class":508},[182,1354,1343],{"class":1342},[182,1356,298],{"class":227},[182,1358,1359,1362,1365,1367],{"class":184,"line":281},[182,1360,1361],{"class":1035},"    name",[182,1363,1364],{"class":508},"?:",[182,1366,1343],{"class":1342},[182,1368,298],{"class":227},[182,1370,1371,1374,1376,1378],{"class":184,"line":289},[182,1372,1373],{"class":1035},"    picture",[182,1375,1364],{"class":508},[182,1377,1343],{"class":1342},[182,1379,298],{"class":227},[182,1381,1382,1385,1387,1389,1391,1393,1396,1398,1400,1402,1404,1406,1409,1411,1413,1415,1418,1420],{"class":184,"line":390},[182,1383,1384],{"class":1035},"    role",[182,1386,238],{"class":508},[182,1388,332],{"class":247},[182,1390,1054],{"class":192},[182,1392,253],{"class":247},[182,1394,1395],{"class":508}," |",[182,1397,332],{"class":247},[182,1399,1064],{"class":192},[182,1401,253],{"class":247},[182,1403,1395],{"class":508},[182,1405,332],{"class":247},[182,1407,1408],{"class":192},"staff",[182,1410,253],{"class":247},[182,1412,1395],{"class":508},[182,1414,332],{"class":247},[182,1416,1417],{"class":192},"unauthorized",[182,1419,253],{"class":247},[182,1421,298],{"class":227},[182,1423,1424],{"class":184,"line":396},[182,1425,1257],{"class":227},[182,1427,1428],{"class":184,"line":407},[182,1429,1430],{"class":227},"}\n",[171,1432,1433],{"id":1433},"使用擴充後的型別",[156,1435,1437],{"className":203,"code":1436,"language":205,"meta":164,"style":164},"const { user } = await requireUserSession(event);\n\u002F\u002F user.role 現在有完整的類型提示：'admin' | 'manager' | 'staff' | 'unauthorized'\n\nif (user.role === \"admin\") {\n  \u002F\u002F 管理員專屬邏輯\n}\n",[26,1438,1439,1461,1466,1470,1497,1502],{"__ignoreMap":164},[182,1440,1441,1443,1445,1447,1449,1451,1453,1456,1459],{"class":184,"line":185},[182,1442,694],{"class":693},[182,1444,320],{"class":227},[182,1446,907],{"class":436},[182,1448,326],{"class":227},[182,1450,753],{"class":508},[182,1452,1189],{"class":212},[182,1454,1455],{"class":219}," requireUserSession",[182,1457,1458],{"class":223},"(event)",[182,1460,298],{"class":227},[182,1462,1463],{"class":184,"line":231},[182,1464,1465],{"class":271},"\u002F\u002F user.role 現在有完整的類型提示：'admin' | 'manager' | 'staff' | 'unauthorized'\n",[182,1467,1468],{"class":184,"line":244},[182,1469,345],{"emptyLinePlaceholder":344},[182,1471,1472,1475,1478,1480,1483,1486,1488,1490,1492,1495],{"class":184,"line":259},[182,1473,1474],{"class":212},"if",[182,1476,1477],{"class":223}," (user",[182,1479,428],{"class":227},[182,1481,1482],{"class":223},"role ",[182,1484,1485],{"class":508},"===",[182,1487,332],{"class":247},[182,1489,1054],{"class":192},[182,1491,253],{"class":247},[182,1493,1494],{"class":223},") ",[182,1496,228],{"class":227},[182,1498,1499],{"class":184,"line":275},[182,1500,1501],{"class":271},"  \u002F\u002F 管理員專屬邏輯\n",[182,1503,1504],{"class":184,"line":281},[182,1505,1430],{"class":227},[43,1507],{},[11,1509,1510],{"id":1510},"路由保護",[171,1512,1514],{"id":1513},"nuxtconfigts-routerules","nuxt.config.ts routeRules",[156,1516,1518],{"className":203,"code":1517,"language":205,"meta":164,"style":164},"export default defineNuxtConfig({\n  routeRules: {\n    \"\u002Fadmin\u002F**\": { auth: { user: { role: \"admin\" } } },\n    \"\u002Flogin\": { auth: \"guest\" }, \u002F\u002F 僅訪客可存取\n    \"\u002Fdashboard\u002F**\": { auth: \"user\" }, \u002F\u002F 需登入\n  },\n});\n",[26,1519,1520,1532,1541,1583,1613,1641,1645],{"__ignoreMap":164},[182,1521,1522,1524,1526,1528,1530],{"class":184,"line":185},[182,1523,213],{"class":212},[182,1525,216],{"class":212},[182,1527,220],{"class":219},[182,1529,224],{"class":223},[182,1531,228],{"class":227},[182,1533,1534,1537,1539],{"class":184,"line":231},[182,1535,1536],{"class":234},"  routeRules",[182,1538,238],{"class":227},[182,1540,404],{"class":227},[182,1542,1543,1545,1548,1550,1552,1554,1557,1559,1561,1563,1565,1567,1569,1571,1573,1575,1577,1579,1581],{"class":184,"line":244},[182,1544,248],{"class":247},[182,1546,1547],{"class":1115},"\u002Fadmin\u002F**",[182,1549,253],{"class":247},[182,1551,238],{"class":227},[182,1553,320],{"class":227},[182,1555,1556],{"class":234}," auth",[182,1558,238],{"class":227},[182,1560,320],{"class":227},[182,1562,907],{"class":234},[182,1564,238],{"class":227},[182,1566,320],{"class":227},[182,1568,1043],{"class":234},[182,1570,238],{"class":227},[182,1572,332],{"class":247},[182,1574,1054],{"class":192},[182,1576,253],{"class":247},[182,1578,326],{"class":227},[182,1580,326],{"class":227},[182,1582,383],{"class":227},[182,1584,1585,1587,1590,1592,1594,1596,1598,1600,1602,1605,1607,1610],{"class":184,"line":259},[182,1586,248],{"class":247},[182,1588,1589],{"class":1115},"\u002Flogin",[182,1591,253],{"class":247},[182,1593,238],{"class":227},[182,1595,320],{"class":227},[182,1597,1556],{"class":234},[182,1599,238],{"class":227},[182,1601,332],{"class":247},[182,1603,1604],{"class":192},"guest",[182,1606,253],{"class":247},[182,1608,1609],{"class":227}," },",[182,1611,1612],{"class":271}," \u002F\u002F 僅訪客可存取\n",[182,1614,1615,1617,1620,1622,1624,1626,1628,1630,1632,1634,1636,1638],{"class":184,"line":275},[182,1616,248],{"class":247},[182,1618,1619],{"class":1115},"\u002Fdashboard\u002F**",[182,1621,253],{"class":247},[182,1623,238],{"class":227},[182,1625,320],{"class":227},[182,1627,1556],{"class":234},[182,1629,238],{"class":227},[182,1631,332],{"class":247},[182,1633,958],{"class":192},[182,1635,253],{"class":247},[182,1637,1609],{"class":227},[182,1639,1640],{"class":271}," \u002F\u002F 需登入\n",[182,1642,1643],{"class":184,"line":281},[182,1644,472],{"class":227},[182,1646,1647,1649,1651],{"class":184,"line":289},[182,1648,292],{"class":227},[182,1650,295],{"class":223},[182,1652,298],{"class":227},[171,1654,1656],{"id":1655},"client-端-middleware","Client 端 Middleware",[156,1658,1660],{"className":203,"code":1659,"language":205,"meta":164,"style":164},"\u002F\u002F app\u002Fmiddleware\u002Fauth.global.ts\nexport default defineNuxtRouteMiddleware(async (to) =&gt; {\n  const { loggedIn, user } = useUserSession()\n\n  \u002F\u002F 公開頁面不需驗證\n  const publicPages = ['\u002Flogin', '\u002Fforbidden']\n  if (publicPages.includes(to.path)) return\n\n  \u002F\u002F 未登入導向登入頁\n  if (!loggedIn.value) {\n    return navigateTo('\u002Flogin')\n  }\n\n  \u002F\u002F 未授權角色導向 forbidden\n  if (user.value?.role === 'unauthorized') {\n    return navigateTo('\u002Fforbidden')\n  }\n})\n",[26,1661,1662,1667,1689,1712,1716,1721,1738,1743,1747,1752,1757,1770,1774,1778,1783,1797,1809,1813],{"__ignoreMap":164},[182,1663,1664],{"class":184,"line":185},[182,1665,1666],{"class":271},"\u002F\u002F app\u002Fmiddleware\u002Fauth.global.ts\n",[182,1668,1669,1671,1673,1676,1678,1680,1683,1685,1687],{"class":184,"line":231},[182,1670,213],{"class":212},[182,1672,216],{"class":212},[182,1674,1675],{"class":219}," defineNuxtRouteMiddleware",[182,1677,224],{"class":223},[182,1679,885],{"class":219},[182,1681,1682],{"class":223}," (to) ",[182,1684,891],{"class":508},[182,1686,894],{"class":223},[182,1688,228],{"class":227},[182,1690,1691,1693,1695,1698,1700,1702,1704,1707,1710],{"class":184,"line":244},[182,1692,901],{"class":223},[182,1694,904],{"class":227},[182,1696,1697],{"class":223}," loggedIn",[182,1699,268],{"class":227},[182,1701,907],{"class":223},[182,1703,326],{"class":227},[182,1705,1706],{"class":223}," = ",[182,1708,1709],{"class":915},"useUserSession",[182,1711,1096],{"class":227},[182,1713,1714],{"class":184,"line":259},[182,1715,345],{"emptyLinePlaceholder":344},[182,1717,1718],{"class":184,"line":275},[182,1719,1720],{"class":271},"  \u002F\u002F 公開頁面不需驗證\n",[182,1722,1723,1726,1728,1730,1733,1735],{"class":184,"line":281},[182,1724,1725],{"class":223},"  const publicPages = ['\u002Flogin'",[182,1727,268],{"class":227},[182,1729,1061],{"class":247},[182,1731,1732],{"class":1115},"\u002Fforbidden",[182,1734,1051],{"class":247},[182,1736,1737],{"class":234},"]\n",[182,1739,1740],{"class":184,"line":289},[182,1741,1742],{"class":234},"  if (publicPages.includes(to.path)) return\n",[182,1744,1745],{"class":184,"line":390},[182,1746,345],{"emptyLinePlaceholder":344},[182,1748,1749],{"class":184,"line":396},[182,1750,1751],{"class":271},"  \u002F\u002F 未登入導向登入頁\n",[182,1753,1754],{"class":184,"line":407},[182,1755,1756],{"class":234},"  if (!loggedIn.value) {\n",[182,1758,1759,1762,1764,1766,1768],{"class":184,"line":417},[182,1760,1761],{"class":234},"    return navigateTo(",[182,1763,1051],{"class":247},[182,1765,1589],{"class":1115},[182,1767,1051],{"class":247},[182,1769,925],{"class":234},[182,1771,1772],{"class":184,"line":442},[182,1773,1257],{"class":234},[182,1775,1776],{"class":184,"line":463},[182,1777,345],{"emptyLinePlaceholder":344},[182,1779,1780],{"class":184,"line":469},[182,1781,1782],{"class":271},"  \u002F\u002F 未授權角色導向 forbidden\n",[182,1784,1785,1788,1790,1792,1794],{"class":184,"line":475},[182,1786,1787],{"class":234},"  if (user.value?.role === ",[182,1789,1051],{"class":247},[182,1791,1417],{"class":1115},[182,1793,1051],{"class":247},[182,1795,1796],{"class":234},") {\n",[182,1798,1799,1801,1803,1805,1807],{"class":184,"line":480},[182,1800,1761],{"class":234},[182,1802,1051],{"class":247},[182,1804,1732],{"class":1115},[182,1806,1051],{"class":247},[182,1808,925],{"class":234},[182,1810,1811],{"class":184,"line":486},[182,1812,1257],{"class":234},[182,1814,1815],{"class":184,"line":496},[182,1816,1144],{"class":234},[43,1818],{},[11,1820,1821],{"id":1821},"環境變數設定",[156,1823,1825],{"className":176,"code":1824,"language":178,"meta":164,"style":164},"# .env\n# Session 密鑰（至少 32 字元）\nNUXT_SESSION_PASSWORD=your-super-secret-session-password-here\n\n# Google OAuth（從 Google Cloud Console 取得）\nNUXT_OAUTH_GOOGLE_CLIENT_ID=your-google-client-id\nNUXT_OAUTH_GOOGLE_CLIENT_SECRET=your-google-client-secret\n",[26,1826,1827,1832,1837,1847,1851,1856,1865],{"__ignoreMap":164},[182,1828,1829],{"class":184,"line":185},[182,1830,1831],{"class":271},"# .env\n",[182,1833,1834],{"class":184,"line":231},[182,1835,1836],{"class":271},"# Session 密鑰（至少 32 字元）\n",[182,1838,1839,1842,1844],{"class":184,"line":244},[182,1840,1841],{"class":223},"NUXT_SESSION_PASSWORD",[182,1843,1186],{"class":508},[182,1845,1846],{"class":192},"your-super-secret-session-password-here\n",[182,1848,1849],{"class":184,"line":259},[182,1850,345],{"emptyLinePlaceholder":344},[182,1852,1853],{"class":184,"line":275},[182,1854,1855],{"class":271},"# Google OAuth（從 Google Cloud Console 取得）\n",[182,1857,1858,1860,1862],{"class":184,"line":281},[182,1859,437],{"class":223},[182,1861,1186],{"class":508},[182,1863,1864],{"class":192},"your-google-client-id\n",[182,1866,1867,1869,1871],{"class":184,"line":289},[182,1868,458],{"class":223},[182,1870,1186],{"class":508},[182,1872,1873],{"class":192},"your-google-client-secret\n",[43,1875],{},[11,1877,1878],{"id":1878},"踩坑經驗",[171,1880,1882],{"id":1881},"session-cookie-設定錯誤","Session Cookie 設定錯誤",[15,1884,1885,1888],{},[136,1886,1887],{},"問題","：登入成功但重新整理後變成未登入。",[15,1890,1891,1894],{},[136,1892,1893],{},"原因","：Cookie 設定不正確，導致無法跨請求保持。",[15,1896,1897,1900],{},[136,1898,1899],{},"解決","：確認以下設定：",[156,1902,1904],{"className":203,"code":1903,"language":205,"meta":164,"style":164},"\u002F\u002F server\u002Fauth.config.ts\nexport default defineServerAuth({\n  session: {\n    expiresIn: 60 * 60 * 24 * 7, \u002F\u002F 設定足夠長的過期時間\n    updateAge: 60 * 60 * 24, \u002F\u002F 自動更新機制\n  },\n});\n",[26,1905,1906,1910,1922,1930,1955,1976,1980],{"__ignoreMap":164},[182,1907,1908],{"class":184,"line":185},[182,1909,312],{"class":271},[182,1911,1912,1914,1916,1918,1920],{"class":184,"line":231},[182,1913,213],{"class":212},[182,1915,216],{"class":212},[182,1917,323],{"class":219},[182,1919,224],{"class":223},[182,1921,228],{"class":227},[182,1923,1924,1926,1928],{"class":184,"line":244},[182,1925,489],{"class":234},[182,1927,238],{"class":227},[182,1929,404],{"class":227},[182,1931,1932,1934,1936,1938,1940,1942,1944,1946,1948,1950,1952],{"class":184,"line":259},[182,1933,499],{"class":234},[182,1935,238],{"class":227},[182,1937,505],{"class":504},[182,1939,509],{"class":508},[182,1941,505],{"class":504},[182,1943,509],{"class":508},[182,1945,516],{"class":504},[182,1947,509],{"class":508},[182,1949,521],{"class":504},[182,1951,268],{"class":227},[182,1953,1954],{"class":271}," \u002F\u002F 設定足夠長的過期時間\n",[182,1956,1957,1959,1961,1963,1965,1967,1969,1971,1973],{"class":184,"line":275},[182,1958,532],{"class":234},[182,1960,238],{"class":227},[182,1962,505],{"class":504},[182,1964,509],{"class":508},[182,1966,505],{"class":504},[182,1968,509],{"class":508},[182,1970,516],{"class":504},[182,1972,268],{"class":227},[182,1974,1975],{"class":271}," \u002F\u002F 自動更新機制\n",[182,1977,1978],{"class":184,"line":281},[182,1979,472],{"class":227},[182,1981,1982,1984,1986],{"class":184,"line":289},[182,1983,292],{"class":227},[182,1985,295],{"class":223},[182,1987,298],{"class":227},[171,1989,1991],{"id":1990},"oauth-callback-url-錯誤","OAuth Callback URL 錯誤",[15,1993,1994,1996],{},[136,1995,1887],{},"：OAuth 登入後導向錯誤頁面。",[15,1998,1999,2001],{},[136,2000,1899],{},"：在 OAuth Provider 設定正確的 Callback URL：",[19,2003,2004,2010],{},[22,2005,2006,2007],{},"本地開發：",[26,2008,2009],{},"http:\u002F\u002Flocalhost:3000\u002Fapi\u002Fauth\u002Fcallback\u002Fgoogle",[22,2011,2012,2013],{},"生產環境：",[26,2014,2015],{},"https:\u002F\u002Fyour-domain.com\u002Fapi\u002Fauth\u002Fcallback\u002Fgoogle",[171,2017,2019],{"id":2018},"忘記處理-unauthorized-角色","忘記處理 unauthorized 角色",[15,2021,2022,2024],{},[136,2023,1887],{},"：新註冊用戶沒有角色，導致存取所有頁面都正常。",[15,2026,2027,2029,2030,2032],{},[136,2028,1899],{},"：預設角色為 ",[26,2031,1417],{},"，並在 middleware 處理：",[156,2034,2036],{"className":203,"code":2035,"language":205,"meta":164,"style":164},"\u002F\u002F middleware\u002Fauth.global.ts\nif (user.value?.role === \"unauthorized\") {\n  return navigateTo(\"\u002Fforbidden\");\n}\n",[26,2037,2038,2043,2071,2091],{"__ignoreMap":164},[182,2039,2040],{"class":184,"line":185},[182,2041,2042],{"class":271},"\u002F\u002F middleware\u002Fauth.global.ts\n",[182,2044,2045,2047,2049,2051,2054,2057,2059,2061,2063,2065,2067,2069],{"class":184,"line":231},[182,2046,1474],{"class":212},[182,2048,1477],{"class":223},[182,2050,428],{"class":227},[182,2052,2053],{"class":223},"value",[182,2055,2056],{"class":227},"?.",[182,2058,1482],{"class":223},[182,2060,1485],{"class":508},[182,2062,332],{"class":247},[182,2064,1417],{"class":192},[182,2066,253],{"class":247},[182,2068,1494],{"class":223},[182,2070,228],{"class":227},[182,2072,2073,2076,2079,2081,2083,2085,2087,2089],{"class":184,"line":244},[182,2074,2075],{"class":212},"  return",[182,2077,2078],{"class":219}," navigateTo",[182,2080,224],{"class":234},[182,2082,253],{"class":247},[182,2084,1732],{"class":192},[182,2086,253],{"class":247},[182,2088,295],{"class":234},[182,2090,298],{"class":227},[182,2092,2093],{"class":184,"line":259},[182,2094,1430],{"class":227},[43,2096],{},[11,2098,2099],{"id":2099},"錯誤處理建議",[56,2101,2102,2115],{},[59,2103,2104],{},[62,2105,2106,2109,2112],{},[65,2107,2108],{},"情境",[65,2110,2111],{},"錯誤碼",[65,2113,2114],{},"建議處理",[75,2116,2117,2130,2143,2154],{},[62,2118,2119,2122,2125],{},[80,2120,2121],{},"未登入",[80,2123,2124],{},"401",[80,2126,2127,2128],{},"導向 ",[26,2129,1589],{},[62,2131,2132,2135,2138],{},[80,2133,2134],{},"無權限",[80,2136,2137],{},"403",[80,2139,2127,2140,2142],{},[26,2141,1732],{}," 或顯示錯誤訊息",[62,2144,2145,2148,2151],{},[80,2146,2147],{},"OAuth 取消",[80,2149,2150],{},"-",[80,2152,2153],{},"提示「登入取消」，留在登入頁",[62,2155,2156,2159,2161],{},[80,2157,2158],{},"Session 過期",[80,2160,2124],{},[80,2162,2163],{},"重新導向登入",[43,2165],{},[11,2167,2168],{"id":2168},"最佳實踐總結",[2170,2171,2172,2181,2187,2193,2199],"ol",{},[22,2173,2174,2177,2178,2180],{},[136,2175,2176],{},"分離認證與資料庫","：使用 ",[26,2179,73],{}," 處理認證，Supabase 僅作資料庫",[22,2182,2183,2186],{},[136,2184,2185],{},"型別定義","：擴充 User 型別以獲得完整的 TypeScript 支援",[22,2188,2189,2192],{},[136,2190,2191],{},"雙重保護","：Client middleware + Server requireUserSession",[22,2194,2195,2198],{},[136,2196,2197],{},"角色檢查","：Server 端永遠要驗證角色，不能只信任 Client",[22,2200,2201,2204],{},[136,2202,2203],{},"預設 unauthorized","：新用戶預設無權限，由管理員授權",[43,2206],{},[11,2208,2209],{"id":2209},"延伸閱讀",[19,2211,2212,2221,2228,2235],{},[22,2213,2214],{},[2215,2216,2220],"a",{"href":2217,"rel":2218},"https:\u002F\u002Fgithub.com\u002Fonmax\u002Fnuxt-better-auth",[2219],"nofollow","nuxt-better-auth 官方文件",[22,2222,2223],{},[2215,2224,2227],{"href":2225,"rel":2226},"https:\u002F\u002Fwww.better-auth.com\u002F",[2219],"Better Auth 文件",[22,2229,2230,2231],{},"上一篇：",[2215,2232,2234],{"href":2233},"\u002Fblog\u002Fnuxt\u002Ftypescript-type-safety","TypeScript 類型安全實戰",[22,2236,2237,2238],{},"下一篇：",[2215,2239,2241],{"href":2240},"\u002Fblog\u002Fnuxt\u002Frole-based-access-control","角色權限系統設計",[2243,2244,2245],"style",{},"html pre.shiki code .sbgvK, html code.shiki .sbgvK{--shiki-light:#E2931D;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s_sjI, html code.shiki .s_sjI{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sVHd0, html code.shiki .sVHd0{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit}html pre.shiki code .sGLFI, html code.shiki .sGLFI{--shiki-light:#6182B8;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .su5hD, html code.shiki .su5hD{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sP7_E, html code.shiki .sP7_E{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .skxfh, html code.shiki .skxfh{--shiki-light:#E53935;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sjJ54, html code.shiki .sjJ54{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sutJx, html code.shiki .sutJx{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit}html pre.shiki code .syTEX, html code.shiki .syTEX{--shiki-light:#FF5370;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s_hVV, html code.shiki .s_hVV{--shiki-light:#90A4AE;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .srdBf, html code.shiki .srdBf{--shiki-light:#F76D47;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .smGrS, html code.shiki .smGrS{--shiki-light:#39ADB5;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sbsja, html code.shiki .sbsja{--shiki-light:#9C3EDA;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVXei, html code.shiki .sVXei{--shiki-light:#E53935;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s99_P, html code.shiki .s99_P{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#E36209;--shiki-default-font-style:inherit;--shiki-dark:#FFAB70;--shiki-dark-font-style:inherit}html pre.shiki code .sucvu, html code.shiki .sucvu{--shiki-light:#E53935;--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sZUrc, html code.shiki .sZUrc{--shiki-light:#E53935;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sZMiF, html code.shiki .sZMiF{--shiki-light:#E2931D;--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":164,"searchDepth":244,"depth":244,"links":2247},[2248,2249,2250,2251,2257,2264,2269,2273,2277,2278,2283,2284,2285],{"id":13,"depth":231,"text":13},{"id":47,"depth":231,"text":48},{"id":154,"depth":231,"text":154},{"id":169,"depth":231,"text":169,"children":2252},[2253,2254,2255,2256],{"id":173,"depth":244,"text":173},{"id":199,"depth":244,"text":200},{"id":301,"depth":244,"text":302},{"id":566,"depth":244,"text":567},{"id":652,"depth":231,"text":653,"children":2258},[2259,2260,2261,2262,2263],{"id":656,"depth":244,"text":657},{"id":682,"depth":244,"text":683},{"id":764,"depth":244,"text":765},{"id":798,"depth":244,"text":799},{"id":837,"depth":244,"text":837},{"id":857,"depth":231,"text":858,"children":2265},[2266,2267,2268],{"id":861,"depth":244,"text":861},{"id":978,"depth":244,"text":978},{"id":1147,"depth":244,"text":1148},{"id":1291,"depth":231,"text":1292,"children":2270},[2271,2272],{"id":1295,"depth":244,"text":1296},{"id":1433,"depth":244,"text":1433},{"id":1510,"depth":231,"text":1510,"children":2274},[2275,2276],{"id":1513,"depth":244,"text":1514},{"id":1655,"depth":244,"text":1656},{"id":1821,"depth":231,"text":1821},{"id":1878,"depth":231,"text":1878,"children":2279},[2280,2281,2282],{"id":1881,"depth":244,"text":1882},{"id":1990,"depth":244,"text":1991},{"id":2018,"depth":244,"text":2019},{"id":2099,"depth":231,"text":2099},{"id":2168,"depth":231,"text":2168},{"id":2209,"depth":231,"text":2209},"Nuxt","2026-01-22","使用 @onmax\u002Fnuxt-better-auth 實現完整的認證系統，包含 OAuth 社交登入、Session 管理與權限檢查。",false,"md",null,{},"\u002Fblog\u002Fnuxt\u002Fbetter-auth-integration",{"title":5,"description":2288},"nuxt-fullstack","Nuxt 4 全棧實戰筆記","blog\u002Fnuxt\u002Fbetter-auth-integration\u002Findex",[2286,2299,2300],"Authentication","OAuth","7Jt9g5RIIOIemsl9ks81SSMwETh9GPVqrlM5bXe5fPo",1780512499430]