[{"data":1,"prerenderedAt":3163},["ShallowReactive",2],{"blog:\u002Fblog\u002Fnuxt\u002Frole-based-access-control":3},{"id":4,"title":5,"author":6,"body":7,"category":3147,"date":3148,"description":3149,"draft":3150,"extension":3151,"image":3152,"meta":3153,"navigation":131,"path":3154,"seo":3155,"series":3156,"seriesOrder":413,"seriesTitle":3157,"stem":3158,"tags":3159,"updatedAt":3152,"__hash__":3162},"blog\u002Fblog\u002Fnuxt\u002Frole-based-access-control\u002Findex.md","角色權限系統設計","charles",{"type":8,"value":9,"toc":3113},"minimark",[10,14,23,39,42,45,49,139,142,213,216,302,304,307,315,325,327,331,335,620,623,926,928,932,935,1042,1045,1206,1209,1539,1541,1545,1549,1802,1805,1859,1861,1864,1868,1895,1899,1949,1951,1954,2501,2503,2506,2510,2516,2526,2678,2682,2687,2693,2792,2795,2800,2805,3030,3032,3035,3072,3074,3077,3109],[11,12,13],"h2",{"id":13},"這篇要解決什麼問題",[15,16,17,18,22],"p",{},"有了認證之後，下一步就是",[19,20,21],"strong",{},"授權","。這篇文章將說明：",[24,25,26,30,33,36],"ul",{},[27,28,29],"li",{},"角色階層的設計考量",[27,31,32],{},"Client 端 middleware 路由守衛",[27,34,35],{},"Server 端 requireUserSession 權限檢查",[27,37,38],{},"nuxt.config.ts 的 routeRules 設定",[40,41],"hr",{},[11,43,44],{"id":44},"角色階層設計",[46,47,48],"h3",{"id":48},"角色定義",[50,51,56],"pre",{"className":52,"code":53,"language":54,"meta":55,"style":55},"language-typescript shiki shiki-themes material-theme-lighter github-light github-dark","\u002F\u002F server\u002Ftypes\u002Fauth.d.ts\ntype UserRole = \"admin\" | \"manager\" | \"staff\" | \"unauthorized\";\n\n\u002F\u002F 角色階層：admin > manager > staff > unauthorized\n","typescript","",[57,58,59,68,126,133],"code",{"__ignoreMap":55},[60,61,64],"span",{"class":62,"line":63},"line",1,[60,65,67],{"class":66},"sutJx","\u002F\u002F server\u002Ftypes\u002Fauth.d.ts\n",[60,69,71,75,79,83,87,91,94,97,99,102,104,106,108,111,113,115,117,120,122],{"class":62,"line":70},2,[60,72,74],{"class":73},"sbsja","type",[60,76,78],{"class":77},"sbgvK"," UserRole",[60,80,82],{"class":81},"smGrS"," =",[60,84,86],{"class":85},"sjJ54"," \"",[60,88,90],{"class":89},"s_sjI","admin",[60,92,93],{"class":85},"\"",[60,95,96],{"class":81}," |",[60,98,86],{"class":85},[60,100,101],{"class":89},"manager",[60,103,93],{"class":85},[60,105,96],{"class":81},[60,107,86],{"class":85},[60,109,110],{"class":89},"staff",[60,112,93],{"class":85},[60,114,96],{"class":81},[60,116,86],{"class":85},[60,118,119],{"class":89},"unauthorized",[60,121,93],{"class":85},[60,123,125],{"class":124},"sP7_E",";\n",[60,127,129],{"class":62,"line":128},3,[60,130,132],{"emptyLinePlaceholder":131},true,"\n",[60,134,136],{"class":62,"line":135},4,[60,137,138],{"class":66},"\u002F\u002F 角色階層：admin > manager > staff > unauthorized\n",[46,140,141],{"id":141},"角色說明",[143,144,145,161],"table",{},[146,147,148],"thead",{},[149,150,151,155,158],"tr",{},[152,153,154],"th",{},"角色",[152,156,157],{},"說明",[152,159,160],{},"典型使用場景",[162,163,164,177,189,201],"tbody",{},[149,165,166,171,174],{},[167,168,169],"td",{},[57,170,90],{},[167,172,173],{},"系統管理員",[167,175,176],{},"完整系統控制權",[149,178,179,183,186],{},[167,180,181],{},[57,182,101],{},[167,184,185],{},"管理者",[167,187,188],{},"管理資源與使用者",[149,190,191,195,198],{},[167,192,193],{},[57,194,110],{},[167,196,197],{},"一般員工",[167,199,200],{},"日常操作權限",[149,202,203,207,210],{},[167,204,205],{},[57,206,119],{},[167,208,209],{},"未授權",[167,211,212],{},"新註冊用戶預設",[46,214,215],{"id":215},"權限矩陣",[143,217,218,233],{},[146,219,220],{},[149,221,222,225,227,229,231],{},[152,223,224],{},"功能",[152,226,90],{},[152,228,101],{},[152,230,110],{},[152,232,119],{},[162,234,235,250,263,276,289],{},[149,236,237,240,243,246,248],{},[167,238,239],{},"系統設定",[167,241,242],{},"✅",[167,244,245],{},"❌",[167,247,245],{},[167,249,245],{},[149,251,252,255,257,259,261],{},[167,253,254],{},"使用者管理",[167,256,242],{},[167,258,242],{},[167,260,245],{},[167,262,245],{},[149,264,265,268,270,272,274],{},[167,266,267],{},"資料編輯",[167,269,242],{},[167,271,242],{},[167,273,242],{},[167,275,245],{},[149,277,278,281,283,285,287],{},[167,279,280],{},"資料檢視",[167,282,242],{},[167,284,242],{},[167,286,242],{},[167,288,245],{},[149,290,291,294,296,298,300],{},[167,292,293],{},"登入頁面",[167,295,245],{},[167,297,245],{},[167,299,245],{},[167,301,242],{},[40,303],{},[11,305,306],{"id":306},"權限檢查架構",[50,308,313],{"className":309,"code":311,"language":312},[310],"language-text","使用者請求\n    │\n    ▼\n┌─────────────────────────────────────────┐\n│        Client Middleware (快速)          │\n├─────────────────────────────────────────┤\n│  - 未登入 → 導向 \u002Flogin                   │\n│  - unauthorized → 導向 \u002Fforbidden         │\n│  - 角色不符 → 導向錯誤頁                   │\n└─────────────────────────────────────────┘\n    │ 通過\n    ▼\n┌─────────────────────────────────────────┐\n│         Server API (安全)                │\n├─────────────────────────────────────────┤\n│  - requireUserSession(event)             │\n│  - requireUserSession(event, {           │\n│      user: { role: ['admin', 'manager'] }│\n│    })                                    │\n└─────────────────────────────────────────┘\n    │ 通過\n    ▼\n執行業務邏輯\n","text",[57,314,311],{"__ignoreMap":55},[15,316,317,320,321,324],{},[19,318,319],{},"重要原則","：Client 端檢查是為了 UX（快速回饋），Server 端檢查是為了安全。",[19,322,323],{},"永遠不能只信任 Client 端","。",[40,326],{},[11,328,330],{"id":329},"client-端路由守衛","Client 端路由守衛",[46,332,334],{"id":333},"global-middleware","Global Middleware",[50,336,338],{"className":52,"code":337,"language":54,"meta":55,"style":55},"\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', '\u002Fregister', '\u002Fforbidden']\n  if (publicPages.includes(to.path)) {\n    return\n  }\n\n  \u002F\u002F 未登入導向登入頁\n  if (!loggedIn.value) {\n    return navigateTo('\u002Flogin')\n  }\n\n  \u002F\u002F unauthorized 角色導向 forbidden\n  if (user.value?.role === 'unauthorized') {\n    return navigateTo('\u002Fforbidden')\n  }\n\n  \u002F\u002F 管理員頁面權限檢查\n  if (to.path.startsWith('\u002Fadmin')) {\n    if (user.value?.role !== 'admin') {\n      return navigateTo('\u002Fforbidden')\n    }\n  }\n})\n",[57,339,340,345,377,407,411,417,448,454,460,466,471,477,483,499,504,509,515,530,543,548,553,559,575,589,603,609,614],{"__ignoreMap":55},[60,341,342],{"class":62,"line":63},[60,343,344],{"class":66},"\u002F\u002F app\u002Fmiddleware\u002Fauth.global.ts\n",[60,346,347,351,354,358,362,365,368,371,374],{"class":62,"line":70},[60,348,350],{"class":349},"sVHd0","export",[60,352,353],{"class":349}," default",[60,355,357],{"class":356},"sGLFI"," defineNuxtRouteMiddleware",[60,359,361],{"class":360},"su5hD","(",[60,363,364],{"class":356},"async",[60,366,367],{"class":360}," (to) ",[60,369,370],{"class":81},"=&",[60,372,373],{"class":360},"gt; ",[60,375,376],{"class":124},"{\n",[60,378,379,382,385,388,391,394,397,400,404],{"class":62,"line":128},[60,380,381],{"class":360},"  const ",[60,383,384],{"class":124},"{",[60,386,387],{"class":360}," loggedIn",[60,389,390],{"class":124},",",[60,392,393],{"class":360}," user",[60,395,396],{"class":124}," }",[60,398,399],{"class":360}," = ",[60,401,403],{"class":402},"sVXei","useUserSession",[60,405,406],{"class":124},"()\n",[60,408,409],{"class":62,"line":135},[60,410,132],{"emptyLinePlaceholder":131},[60,412,414],{"class":62,"line":413},5,[60,415,416],{"class":66},"  \u002F\u002F 公開頁面清單\n",[60,418,420,423,425,428,432,435,437,439,442,444],{"class":62,"line":419},6,[60,421,422],{"class":360},"  const publicPages = ['\u002Flogin'",[60,424,390],{"class":124},[60,426,427],{"class":85}," '",[60,429,431],{"class":430},"sZUrc","\u002Fregister",[60,433,434],{"class":85},"'",[60,436,390],{"class":124},[60,438,427],{"class":85},[60,440,441],{"class":430},"\u002Fforbidden",[60,443,434],{"class":85},[60,445,447],{"class":446},"skxfh","]\n",[60,449,451],{"class":62,"line":450},7,[60,452,453],{"class":446},"  if (publicPages.includes(to.path)) {\n",[60,455,457],{"class":62,"line":456},8,[60,458,459],{"class":446},"    return\n",[60,461,463],{"class":62,"line":462},9,[60,464,465],{"class":446},"  }\n",[60,467,469],{"class":62,"line":468},10,[60,470,132],{"emptyLinePlaceholder":131},[60,472,474],{"class":62,"line":473},11,[60,475,476],{"class":66},"  \u002F\u002F 未登入導向登入頁\n",[60,478,480],{"class":62,"line":479},12,[60,481,482],{"class":446},"  if (!loggedIn.value) {\n",[60,484,486,489,491,494,496],{"class":62,"line":485},13,[60,487,488],{"class":446},"    return navigateTo(",[60,490,434],{"class":85},[60,492,493],{"class":430},"\u002Flogin",[60,495,434],{"class":85},[60,497,498],{"class":446},")\n",[60,500,502],{"class":62,"line":501},14,[60,503,465],{"class":446},[60,505,507],{"class":62,"line":506},15,[60,508,132],{"emptyLinePlaceholder":131},[60,510,512],{"class":62,"line":511},16,[60,513,514],{"class":66},"  \u002F\u002F unauthorized 角色導向 forbidden\n",[60,516,518,521,523,525,527],{"class":62,"line":517},17,[60,519,520],{"class":446},"  if (user.value?.role === ",[60,522,434],{"class":85},[60,524,119],{"class":430},[60,526,434],{"class":85},[60,528,529],{"class":446},") {\n",[60,531,533,535,537,539,541],{"class":62,"line":532},18,[60,534,488],{"class":446},[60,536,434],{"class":85},[60,538,441],{"class":430},[60,540,434],{"class":85},[60,542,498],{"class":446},[60,544,546],{"class":62,"line":545},19,[60,547,465],{"class":446},[60,549,551],{"class":62,"line":550},20,[60,552,132],{"emptyLinePlaceholder":131},[60,554,556],{"class":62,"line":555},21,[60,557,558],{"class":66},"  \u002F\u002F 管理員頁面權限檢查\n",[60,560,562,565,567,570,572],{"class":62,"line":561},22,[60,563,564],{"class":446},"  if (to.path.startsWith(",[60,566,434],{"class":85},[60,568,569],{"class":430},"\u002Fadmin",[60,571,434],{"class":85},[60,573,574],{"class":446},")) {\n",[60,576,578,581,583,585,587],{"class":62,"line":577},23,[60,579,580],{"class":446},"    if (user.value?.role !== ",[60,582,434],{"class":85},[60,584,90],{"class":430},[60,586,434],{"class":85},[60,588,529],{"class":446},[60,590,592,595,597,599,601],{"class":62,"line":591},24,[60,593,594],{"class":446},"      return navigateTo(",[60,596,434],{"class":85},[60,598,441],{"class":430},[60,600,434],{"class":85},[60,602,498],{"class":446},[60,604,606],{"class":62,"line":605},25,[60,607,608],{"class":446},"    }\n",[60,610,612],{"class":62,"line":611},26,[60,613,465],{"class":446},[60,615,617],{"class":62,"line":616},27,[60,618,619],{"class":446},"})\n",[46,621,622],{"id":622},"角色階層檢查函式",[50,624,626],{"className":52,"code":625,"language":54,"meta":55,"style":55},"\u002F\u002F app\u002Futils\u002Fpermission.ts\nconst ROLE_HIERARCHY: Record&lt;string, number&gt; = {\n  admin: 4,\n  manager: 3,\n  staff: 2,\n  unauthorized: 1,\n}\n\nexport function hasRole(\n  userRole: string | undefined,\n  requiredRole: string\n): boolean {\n  if (!userRole) return false\n  return (ROLE_HIERARCHY[userRole] ?? 0) &gt;= (ROLE_HIERARCHY[requiredRole] ?? 0)\n}\n\n\u002F\u002F 使用範例\nif (hasRole(user.value?.role, 'manager')) {\n  \u002F\u002F 有 manager 或更高權限\n}\n",[57,627,628,633,677,691,703,715,727,732,736,749,768,778,790,814,866,870,874,879,917,922],{"__ignoreMap":55},[60,629,630],{"class":62,"line":63},[60,631,632],{"class":66},"\u002F\u002F app\u002Futils\u002Fpermission.ts\n",[60,634,635,638,642,645,648,651,654,657,660,662,665,667,670,672,674],{"class":62,"line":70},[60,636,637],{"class":73},"const",[60,639,641],{"class":640},"s_hVV"," ROLE_HIERARCHY",[60,643,644],{"class":81},":",[60,646,647],{"class":77}," Record",[60,649,650],{"class":81},"&",[60,652,653],{"class":77},"lt",[60,655,656],{"class":124},";",[60,658,659],{"class":360},"string",[60,661,390],{"class":124},[60,663,664],{"class":360}," number",[60,666,650],{"class":81},[60,668,669],{"class":360},"gt",[60,671,656],{"class":124},[60,673,82],{"class":81},[60,675,676],{"class":124}," {\n",[60,678,679,682,684,688],{"class":62,"line":128},[60,680,681],{"class":446},"  admin",[60,683,644],{"class":124},[60,685,687],{"class":686},"srdBf"," 4",[60,689,690],{"class":124},",\n",[60,692,693,696,698,701],{"class":62,"line":135},[60,694,695],{"class":446},"  manager",[60,697,644],{"class":124},[60,699,700],{"class":686}," 3",[60,702,690],{"class":124},[60,704,705,708,710,713],{"class":62,"line":413},[60,706,707],{"class":446},"  staff",[60,709,644],{"class":124},[60,711,712],{"class":686}," 2",[60,714,690],{"class":124},[60,716,717,720,722,725],{"class":62,"line":419},[60,718,719],{"class":446},"  unauthorized",[60,721,644],{"class":124},[60,723,724],{"class":686}," 1",[60,726,690],{"class":124},[60,728,729],{"class":62,"line":450},[60,730,731],{"class":124},"}\n",[60,733,734],{"class":62,"line":456},[60,735,132],{"emptyLinePlaceholder":131},[60,737,738,740,743,746],{"class":62,"line":462},[60,739,350],{"class":349},[60,741,742],{"class":73}," function",[60,744,745],{"class":356}," hasRole",[60,747,748],{"class":124},"(\n",[60,750,751,755,757,761,763,766],{"class":62,"line":468},[60,752,754],{"class":753},"s99_P","  userRole",[60,756,644],{"class":81},[60,758,760],{"class":759},"sZMiF"," string",[60,762,96],{"class":81},[60,764,765],{"class":759}," undefined",[60,767,690],{"class":124},[60,769,770,773,775],{"class":62,"line":473},[60,771,772],{"class":753},"  requiredRole",[60,774,644],{"class":81},[60,776,777],{"class":759}," string\n",[60,779,780,783,785,788],{"class":62,"line":479},[60,781,782],{"class":124},")",[60,784,644],{"class":81},[60,786,787],{"class":759}," boolean",[60,789,676],{"class":124},[60,791,792,795,798,801,804,807,810],{"class":62,"line":485},[60,793,794],{"class":349},"  if",[60,796,797],{"class":446}," (",[60,799,800],{"class":81},"!",[60,802,803],{"class":360},"userRole",[60,805,806],{"class":446},") ",[60,808,809],{"class":349},"return",[60,811,813],{"class":812},"syTEX"," false\n",[60,815,816,819,821,824,827,829,832,835,838,840,842,844,846,849,851,853,855,858,860,862,864],{"class":62,"line":501},[60,817,818],{"class":349},"  return",[60,820,797],{"class":446},[60,822,823],{"class":640},"ROLE_HIERARCHY",[60,825,826],{"class":446},"[",[60,828,803],{"class":360},[60,830,831],{"class":446},"] ",[60,833,834],{"class":81},"??",[60,836,837],{"class":686}," 0",[60,839,806],{"class":446},[60,841,650],{"class":81},[60,843,669],{"class":360},[60,845,656],{"class":124},[60,847,848],{"class":81},"=",[60,850,797],{"class":446},[60,852,823],{"class":640},[60,854,826],{"class":446},[60,856,857],{"class":360},"requiredRole",[60,859,831],{"class":446},[60,861,834],{"class":81},[60,863,837],{"class":686},[60,865,498],{"class":446},[60,867,868],{"class":62,"line":506},[60,869,731],{"class":124},[60,871,872],{"class":62,"line":511},[60,873,132],{"emptyLinePlaceholder":131},[60,875,876],{"class":62,"line":517},[60,877,878],{"class":66},"\u002F\u002F 使用範例\n",[60,880,881,884,886,889,892,895,898,901,904,906,908,910,912,915],{"class":62,"line":532},[60,882,883],{"class":349},"if",[60,885,797],{"class":360},[60,887,888],{"class":356},"hasRole",[60,890,891],{"class":360},"(user",[60,893,894],{"class":124},".",[60,896,897],{"class":360},"value",[60,899,900],{"class":124},"?.",[60,902,903],{"class":360},"role",[60,905,390],{"class":124},[60,907,427],{"class":85},[60,909,101],{"class":89},[60,911,434],{"class":85},[60,913,914],{"class":360},")) ",[60,916,376],{"class":124},[60,918,919],{"class":62,"line":545},[60,920,921],{"class":66},"  \u002F\u002F 有 manager 或更高權限\n",[60,923,924],{"class":62,"line":550},[60,925,731],{"class":124},[40,927],{},[11,929,931],{"id":930},"server-端權限檢查","Server 端權限檢查",[46,933,934],{"id":934},"基本用法",[50,936,938],{"className":52,"code":937,"language":54,"meta":55,"style":55},"\u002F\u002F server\u002Fapi\u002Fv1\u002Fprofile.get.ts\nexport default defineEventHandler(async (event) =&gt; {\n  \u002F\u002F 只要求登入\n  const { user } = await requireUserSession(event)\n\n  return { name: user.name, email: user.email }\n})\n",[57,939,940,945,967,972,995,999,1035],{"__ignoreMap":55},[60,941,942],{"class":62,"line":63},[60,943,944],{"class":66},"\u002F\u002F server\u002Fapi\u002Fv1\u002Fprofile.get.ts\n",[60,946,947,949,951,954,956,958,961,963,965],{"class":62,"line":70},[60,948,350],{"class":349},[60,950,353],{"class":349},[60,952,953],{"class":356}," defineEventHandler",[60,955,361],{"class":360},[60,957,364],{"class":356},[60,959,960],{"class":360}," (event) ",[60,962,370],{"class":81},[60,964,373],{"class":360},[60,966,376],{"class":124},[60,968,969],{"class":62,"line":128},[60,970,971],{"class":66},"  \u002F\u002F 只要求登入\n",[60,973,974,976,978,980,982,985,988,990,993],{"class":62,"line":135},[60,975,381],{"class":360},[60,977,384],{"class":124},[60,979,393],{"class":360},[60,981,396],{"class":124},[60,983,984],{"class":360}," = await ",[60,986,987],{"class":402},"requireUserSession",[60,989,361],{"class":124},[60,991,992],{"class":753},"event",[60,994,498],{"class":124},[60,996,997],{"class":62,"line":413},[60,998,132],{"emptyLinePlaceholder":131},[60,1000,1001,1004,1006,1009,1011,1013,1015,1018,1020,1023,1025,1027,1029,1032],{"class":62,"line":419},[60,1002,1003],{"class":360},"  return ",[60,1005,384],{"class":124},[60,1007,1008],{"class":77}," name",[60,1010,644],{"class":124},[60,1012,393],{"class":360},[60,1014,894],{"class":124},[60,1016,1017],{"class":360},"name",[60,1019,390],{"class":124},[60,1021,1022],{"class":77}," email",[60,1024,644],{"class":124},[60,1026,393],{"class":360},[60,1028,894],{"class":124},[60,1030,1031],{"class":360},"email",[60,1033,1034],{"class":124}," }\n",[60,1036,1037,1040],{"class":62,"line":450},[60,1038,1039],{"class":124},"}",[60,1041,498],{"class":360},[46,1043,1044],{"id":1044},"要求特定角色",[50,1046,1048],{"className":52,"code":1047,"language":54,"meta":55,"style":55},"\u002F\u002F server\u002Fapi\u002Fadmin\u002Fusers.get.ts\nexport default defineEventHandler(async (event) =&gt; {\n  \u002F\u002F 要求 admin 或 manager 角色\n  const { user } = await requireUserSession(event, {\n    user: { role: ['admin', 'manager'] },\n  })\n\n  const supabase = getServerSupabaseClient()\n  const { data } = await supabase.from('users').select('*')\n\n  return { data }\n})\n",[57,1049,1050,1055,1075,1080,1102,1141,1146,1150,1160,1193,1197,1202],{"__ignoreMap":55},[60,1051,1052],{"class":62,"line":63},[60,1053,1054],{"class":66},"\u002F\u002F server\u002Fapi\u002Fadmin\u002Fusers.get.ts\n",[60,1056,1057,1059,1061,1063,1065,1067,1069,1071,1073],{"class":62,"line":70},[60,1058,350],{"class":349},[60,1060,353],{"class":349},[60,1062,953],{"class":356},[60,1064,361],{"class":360},[60,1066,364],{"class":356},[60,1068,960],{"class":360},[60,1070,370],{"class":81},[60,1072,373],{"class":360},[60,1074,376],{"class":124},[60,1076,1077],{"class":62,"line":128},[60,1078,1079],{"class":66},"  \u002F\u002F 要求 admin 或 manager 角色\n",[60,1081,1082,1084,1086,1088,1090,1092,1094,1096,1098,1100],{"class":62,"line":135},[60,1083,381],{"class":360},[60,1085,384],{"class":124},[60,1087,393],{"class":360},[60,1089,396],{"class":124},[60,1091,984],{"class":360},[60,1093,987],{"class":402},[60,1095,361],{"class":124},[60,1097,992],{"class":753},[60,1099,390],{"class":124},[60,1101,676],{"class":124},[60,1103,1104,1108,1110,1113,1116,1118,1121,1123,1125,1127,1129,1131,1133,1135,1138],{"class":62,"line":413},[60,1105,1107],{"class":1106},"sucvu","    user",[60,1109,644],{"class":124},[60,1111,1112],{"class":124}," {",[60,1114,1115],{"class":1106}," role",[60,1117,644],{"class":124},[60,1119,1120],{"class":124}," [",[60,1122,434],{"class":85},[60,1124,90],{"class":89},[60,1126,434],{"class":85},[60,1128,390],{"class":124},[60,1130,427],{"class":85},[60,1132,101],{"class":89},[60,1134,434],{"class":85},[60,1136,1137],{"class":124},"]",[60,1139,1140],{"class":124}," },\n",[60,1142,1143],{"class":62,"line":419},[60,1144,1145],{"class":124},"  })\n",[60,1147,1148],{"class":62,"line":450},[60,1149,132],{"emptyLinePlaceholder":131},[60,1151,1152,1155,1158],{"class":62,"line":456},[60,1153,1154],{"class":360},"  const supabase = ",[60,1156,1157],{"class":402},"getServerSupabaseClient",[60,1159,406],{"class":124},[60,1161,1162,1164,1166,1169,1171,1174,1176,1179,1181,1184,1186,1189,1191],{"class":62,"line":462},[60,1163,381],{"class":360},[60,1165,384],{"class":124},[60,1167,1168],{"class":360}," data",[60,1170,396],{"class":124},[60,1172,1173],{"class":360}," = await supabase.from(",[60,1175,434],{"class":85},[60,1177,1178],{"class":430},"users",[60,1180,434],{"class":85},[60,1182,1183],{"class":446},").select(",[60,1185,434],{"class":85},[60,1187,1188],{"class":430},"*",[60,1190,434],{"class":85},[60,1192,498],{"class":446},[60,1194,1195],{"class":62,"line":468},[60,1196,132],{"emptyLinePlaceholder":131},[60,1198,1199],{"class":62,"line":473},[60,1200,1201],{"class":446},"  return { data }\n",[60,1203,1204],{"class":62,"line":479},[60,1205,619],{"class":446},[46,1207,1208],{"id":1208},"手動權限檢查",[50,1210,1212],{"className":52,"code":1211,"language":54,"meta":55,"style":55},"\u002F\u002F server\u002Fapi\u002Fv1\u002Fresources\u002F[resourceId].patch.ts\nexport default defineEventHandler(async (event) =&gt; {\n  const { user } = await requireUserSession(event)\n  const resourceId = getRouterParam(event, 'resourceId')\n\n  \u002F\u002F 取得資源\n  const supabase = getServerSupabaseClient()\n  const { data: resource } = await supabase\n    .from('resources')\n    .select('owner_id')\n    .eq('id', resourceId)\n    .single()\n\n  \u002F\u002F 檢查權限：擁有者或管理員可編輯\n  const isOwner = resource?.owner_id === user.id\n  const isAdmin = ['admin', 'manager'].includes(user.role)\n\n  if (!isOwner &amp;&amp; !isAdmin) {\n    throw createError({\n      statusCode: 403,\n      message: '您沒有權限編輯此資源',\n    })\n  }\n\n  \u002F\u002F 執行更新...\n})\n",[57,1213,1214,1219,1239,1259,1282,1286,1291,1299,1317,1331,1345,1362,1367,1371,1376,1400,1437,1441,1473,1485,1497,1513,1520,1524,1528,1533],{"__ignoreMap":55},[60,1215,1216],{"class":62,"line":63},[60,1217,1218],{"class":66},"\u002F\u002F server\u002Fapi\u002Fv1\u002Fresources\u002F[resourceId].patch.ts\n",[60,1220,1221,1223,1225,1227,1229,1231,1233,1235,1237],{"class":62,"line":70},[60,1222,350],{"class":349},[60,1224,353],{"class":349},[60,1226,953],{"class":356},[60,1228,361],{"class":360},[60,1230,364],{"class":356},[60,1232,960],{"class":360},[60,1234,370],{"class":81},[60,1236,373],{"class":360},[60,1238,376],{"class":124},[60,1240,1241,1243,1245,1247,1249,1251,1253,1255,1257],{"class":62,"line":128},[60,1242,381],{"class":360},[60,1244,384],{"class":124},[60,1246,393],{"class":360},[60,1248,396],{"class":124},[60,1250,984],{"class":360},[60,1252,987],{"class":402},[60,1254,361],{"class":124},[60,1256,992],{"class":753},[60,1258,498],{"class":124},[60,1260,1261,1264,1267,1269,1271,1273,1275,1278,1280],{"class":62,"line":135},[60,1262,1263],{"class":360},"  const resourceId = ",[60,1265,1266],{"class":402},"getRouterParam",[60,1268,361],{"class":124},[60,1270,992],{"class":753},[60,1272,390],{"class":124},[60,1274,427],{"class":85},[60,1276,1277],{"class":89},"resourceId",[60,1279,434],{"class":85},[60,1281,498],{"class":124},[60,1283,1284],{"class":62,"line":413},[60,1285,132],{"emptyLinePlaceholder":131},[60,1287,1288],{"class":62,"line":419},[60,1289,1290],{"class":66},"  \u002F\u002F 取得資源\n",[60,1292,1293,1295,1297],{"class":62,"line":450},[60,1294,1154],{"class":360},[60,1296,1157],{"class":402},[60,1298,406],{"class":124},[60,1300,1301,1303,1305,1307,1309,1312,1314],{"class":62,"line":456},[60,1302,381],{"class":360},[60,1304,384],{"class":124},[60,1306,1168],{"class":77},[60,1308,644],{"class":124},[60,1310,1311],{"class":360}," resource",[60,1313,396],{"class":124},[60,1315,1316],{"class":360}," = await supabase\n",[60,1318,1319,1322,1324,1327,1329],{"class":62,"line":462},[60,1320,1321],{"class":360},"    .from(",[60,1323,434],{"class":85},[60,1325,1326],{"class":430},"resources",[60,1328,434],{"class":85},[60,1330,498],{"class":446},[60,1332,1333,1336,1338,1341,1343],{"class":62,"line":468},[60,1334,1335],{"class":446},"    .select(",[60,1337,434],{"class":85},[60,1339,1340],{"class":430},"owner_id",[60,1342,434],{"class":85},[60,1344,498],{"class":446},[60,1346,1347,1350,1352,1355,1357,1359],{"class":62,"line":473},[60,1348,1349],{"class":446},"    .eq(",[60,1351,434],{"class":85},[60,1353,1354],{"class":430},"id",[60,1356,434],{"class":85},[60,1358,390],{"class":124},[60,1360,1361],{"class":360}," resourceId)\n",[60,1363,1364],{"class":62,"line":479},[60,1365,1366],{"class":360},"    .single()\n",[60,1368,1369],{"class":62,"line":485},[60,1370,132],{"emptyLinePlaceholder":131},[60,1372,1373],{"class":62,"line":501},[60,1374,1375],{"class":66},"  \u002F\u002F 檢查權限：擁有者或管理員可編輯\n",[60,1377,1378,1381,1383,1385,1387,1390,1393,1395,1397],{"class":62,"line":506},[60,1379,1380],{"class":360},"  const isOwner ",[60,1382,848],{"class":81},[60,1384,1311],{"class":360},[60,1386,900],{"class":124},[60,1388,1389],{"class":360},"owner_id ",[60,1391,1392],{"class":81},"===",[60,1394,393],{"class":360},[60,1396,894],{"class":124},[60,1398,1399],{"class":360},"id\n",[60,1401,1402,1405,1407,1409,1411,1413,1415,1417,1419,1421,1423,1425,1427,1430,1432,1434],{"class":62,"line":511},[60,1403,1404],{"class":360},"  const isAdmin ",[60,1406,848],{"class":81},[60,1408,1120],{"class":360},[60,1410,434],{"class":85},[60,1412,90],{"class":89},[60,1414,434],{"class":85},[60,1416,390],{"class":124},[60,1418,427],{"class":85},[60,1420,101],{"class":89},[60,1422,434],{"class":85},[60,1424,1137],{"class":360},[60,1426,894],{"class":124},[60,1428,1429],{"class":356},"includes",[60,1431,891],{"class":360},[60,1433,894],{"class":124},[60,1435,1436],{"class":360},"role)\n",[60,1438,1439],{"class":62,"line":517},[60,1440,132],{"emptyLinePlaceholder":131},[60,1442,1443,1445,1447,1449,1452,1455,1458,1461,1463,1466,1469,1471],{"class":62,"line":532},[60,1444,794],{"class":402},[60,1446,797],{"class":124},[60,1448,800],{"class":360},[60,1450,1451],{"class":753},"isOwner",[60,1453,1454],{"class":360}," &",[60,1456,1457],{"class":753},"amp",[60,1459,1460],{"class":360},";&",[60,1462,1457],{"class":753},[60,1464,1465],{"class":360},"; !",[60,1467,1468],{"class":753},"isAdmin",[60,1470,782],{"class":124},[60,1472,676],{"class":124},[60,1474,1475,1478,1481,1483],{"class":62,"line":545},[60,1476,1477],{"class":349},"    throw",[60,1479,1480],{"class":356}," createError",[60,1482,361],{"class":446},[60,1484,376],{"class":124},[60,1486,1487,1490,1492,1495],{"class":62,"line":550},[60,1488,1489],{"class":446},"      statusCode",[60,1491,644],{"class":124},[60,1493,1494],{"class":686}," 403",[60,1496,690],{"class":124},[60,1498,1499,1502,1504,1506,1509,1511],{"class":62,"line":555},[60,1500,1501],{"class":446},"      message",[60,1503,644],{"class":124},[60,1505,427],{"class":85},[60,1507,1508],{"class":89},"您沒有權限編輯此資源",[60,1510,434],{"class":85},[60,1512,690],{"class":124},[60,1514,1515,1518],{"class":62,"line":561},[60,1516,1517],{"class":124},"    }",[60,1519,498],{"class":446},[60,1521,1522],{"class":62,"line":577},[60,1523,465],{"class":124},[60,1525,1526],{"class":62,"line":591},[60,1527,132],{"emptyLinePlaceholder":131},[60,1529,1530],{"class":62,"line":605},[60,1531,1532],{"class":66},"  \u002F\u002F 執行更新...\n",[60,1534,1535,1537],{"class":62,"line":611},[60,1536,1039],{"class":124},[60,1538,498],{"class":360},[40,1540],{},[11,1542,1544],{"id":1543},"routerules-設定","routeRules 設定",[46,1546,1548],{"id":1547},"nuxtconfigts","nuxt.config.ts",[50,1550,1552],{"className":52,"code":1551,"language":54,"meta":55,"style":55},"export default defineNuxtConfig({\n  routeRules: {\n    \u002F\u002F 公開頁面\n    \"\u002Flogin\": { auth: \"guest\" },\n    \"\u002Fregister\": { auth: \"guest\" },\n\n    \u002F\u002F 需要登入\n    \"\u002Fdashboard\u002F**\": { auth: \"user\" },\n\n    \u002F\u002F 管理員專用\n    \"\u002Fadmin\u002F**\": {\n      auth: { user: { role: \"admin\" } },\n    },\n\n    \u002F\u002F 管理者以上\n    \"\u002Fmanage\u002F**\": {\n      auth: { user: { role: [\"admin\", \"manager\"] } },\n    },\n  },\n});\n",[57,1553,1554,1567,1576,1581,1608,1632,1636,1641,1667,1671,1676,1689,1718,1723,1727,1732,1745,1785,1789,1794],{"__ignoreMap":55},[60,1555,1556,1558,1560,1563,1565],{"class":62,"line":63},[60,1557,350],{"class":349},[60,1559,353],{"class":349},[60,1561,1562],{"class":356}," defineNuxtConfig",[60,1564,361],{"class":360},[60,1566,376],{"class":124},[60,1568,1569,1572,1574],{"class":62,"line":70},[60,1570,1571],{"class":446},"  routeRules",[60,1573,644],{"class":124},[60,1575,676],{"class":124},[60,1577,1578],{"class":62,"line":128},[60,1579,1580],{"class":66},"    \u002F\u002F 公開頁面\n",[60,1582,1583,1586,1588,1590,1592,1594,1597,1599,1601,1604,1606],{"class":62,"line":135},[60,1584,1585],{"class":85},"    \"",[60,1587,493],{"class":430},[60,1589,93],{"class":85},[60,1591,644],{"class":124},[60,1593,1112],{"class":124},[60,1595,1596],{"class":446}," auth",[60,1598,644],{"class":124},[60,1600,86],{"class":85},[60,1602,1603],{"class":89},"guest",[60,1605,93],{"class":85},[60,1607,1140],{"class":124},[60,1609,1610,1612,1614,1616,1618,1620,1622,1624,1626,1628,1630],{"class":62,"line":413},[60,1611,1585],{"class":85},[60,1613,431],{"class":430},[60,1615,93],{"class":85},[60,1617,644],{"class":124},[60,1619,1112],{"class":124},[60,1621,1596],{"class":446},[60,1623,644],{"class":124},[60,1625,86],{"class":85},[60,1627,1603],{"class":89},[60,1629,93],{"class":85},[60,1631,1140],{"class":124},[60,1633,1634],{"class":62,"line":419},[60,1635,132],{"emptyLinePlaceholder":131},[60,1637,1638],{"class":62,"line":450},[60,1639,1640],{"class":66},"    \u002F\u002F 需要登入\n",[60,1642,1643,1645,1648,1650,1652,1654,1656,1658,1660,1663,1665],{"class":62,"line":456},[60,1644,1585],{"class":85},[60,1646,1647],{"class":430},"\u002Fdashboard\u002F**",[60,1649,93],{"class":85},[60,1651,644],{"class":124},[60,1653,1112],{"class":124},[60,1655,1596],{"class":446},[60,1657,644],{"class":124},[60,1659,86],{"class":85},[60,1661,1662],{"class":89},"user",[60,1664,93],{"class":85},[60,1666,1140],{"class":124},[60,1668,1669],{"class":62,"line":462},[60,1670,132],{"emptyLinePlaceholder":131},[60,1672,1673],{"class":62,"line":468},[60,1674,1675],{"class":66},"    \u002F\u002F 管理員專用\n",[60,1677,1678,1680,1683,1685,1687],{"class":62,"line":473},[60,1679,1585],{"class":85},[60,1681,1682],{"class":430},"\u002Fadmin\u002F**",[60,1684,93],{"class":85},[60,1686,644],{"class":124},[60,1688,676],{"class":124},[60,1690,1691,1694,1696,1698,1700,1702,1704,1706,1708,1710,1712,1714,1716],{"class":62,"line":479},[60,1692,1693],{"class":446},"      auth",[60,1695,644],{"class":124},[60,1697,1112],{"class":124},[60,1699,393],{"class":446},[60,1701,644],{"class":124},[60,1703,1112],{"class":124},[60,1705,1115],{"class":446},[60,1707,644],{"class":124},[60,1709,86],{"class":85},[60,1711,90],{"class":89},[60,1713,93],{"class":85},[60,1715,396],{"class":124},[60,1717,1140],{"class":124},[60,1719,1720],{"class":62,"line":485},[60,1721,1722],{"class":124},"    },\n",[60,1724,1725],{"class":62,"line":501},[60,1726,132],{"emptyLinePlaceholder":131},[60,1728,1729],{"class":62,"line":506},[60,1730,1731],{"class":66},"    \u002F\u002F 管理者以上\n",[60,1733,1734,1736,1739,1741,1743],{"class":62,"line":511},[60,1735,1585],{"class":85},[60,1737,1738],{"class":430},"\u002Fmanage\u002F**",[60,1740,93],{"class":85},[60,1742,644],{"class":124},[60,1744,676],{"class":124},[60,1746,1747,1749,1751,1753,1755,1757,1759,1761,1763,1765,1767,1769,1771,1773,1775,1777,1779,1781,1783],{"class":62,"line":517},[60,1748,1693],{"class":446},[60,1750,644],{"class":124},[60,1752,1112],{"class":124},[60,1754,393],{"class":446},[60,1756,644],{"class":124},[60,1758,1112],{"class":124},[60,1760,1115],{"class":446},[60,1762,644],{"class":124},[60,1764,1120],{"class":360},[60,1766,93],{"class":85},[60,1768,90],{"class":89},[60,1770,93],{"class":85},[60,1772,390],{"class":124},[60,1774,86],{"class":85},[60,1776,101],{"class":89},[60,1778,93],{"class":85},[60,1780,831],{"class":360},[60,1782,1039],{"class":124},[60,1784,1140],{"class":124},[60,1786,1787],{"class":62,"line":532},[60,1788,1722],{"class":124},[60,1790,1791],{"class":62,"line":545},[60,1792,1793],{"class":124},"  },\n",[60,1795,1796,1798,1800],{"class":62,"line":550},[60,1797,1039],{"class":124},[60,1799,782],{"class":360},[60,1801,125],{"class":124},[46,1803,1804],{"id":1804},"路由規則說明",[143,1806,1807,1817],{},[146,1808,1809],{},[149,1810,1811,1814],{},[152,1812,1813],{},"規則",[152,1815,1816],{},"意義",[162,1818,1819,1829,1839,1849],{},[149,1820,1821,1826],{},[167,1822,1823],{},[57,1824,1825],{},"auth: 'guest'",[167,1827,1828],{},"僅未登入用戶可存取",[149,1830,1831,1836],{},[167,1832,1833],{},[57,1834,1835],{},"auth: 'user'",[167,1837,1838],{},"需要登入（任何角色）",[149,1840,1841,1846],{},[167,1842,1843],{},[57,1844,1845],{},"auth: { user: { role: 'admin' } }",[167,1847,1848],{},"需要特定角色",[149,1850,1851,1856],{},[167,1852,1853],{},[57,1854,1855],{},"auth: { user: { role: ['a', 'b'] } }",[167,1857,1858],{},"需要任一角色",[40,1860],{},[11,1862,1863],{"id":1863},"頁面層級權限",[46,1865,1867],{"id":1866},"使用-definepagemeta","使用 definePageMeta",[50,1869,1873],{"className":1870,"code":1871,"language":1872,"meta":55,"style":55},"language-vue shiki shiki-themes material-theme-lighter github-light github-dark","&lt;!-- pages\u002Fadmin\u002Fsettings.vue --&gt; &lt;script setup lang=\"ts\"&gt;\ndefinePageMeta({ auth: { user: { role: 'admin' } } }) &lt;\u002Fscript&gt;\n&lt;template&gt; &lt;div&gt; &lt;h1&gt;系統設定&lt;\u002Fh1&gt; &lt;!-- 只有 admin\n看得到 --&gt; &lt;\u002Fdiv&gt; &lt;\u002Ftemplate&gt;\n","vue",[57,1874,1875,1880,1885,1890],{"__ignoreMap":55},[60,1876,1877],{"class":62,"line":63},[60,1878,1879],{"class":360},"&lt;!-- pages\u002Fadmin\u002Fsettings.vue --&gt; &lt;script setup lang=\"ts\"&gt;\n",[60,1881,1882],{"class":62,"line":70},[60,1883,1884],{"class":360},"definePageMeta({ auth: { user: { role: 'admin' } } }) &lt;\u002Fscript&gt;\n",[60,1886,1887],{"class":62,"line":128},[60,1888,1889],{"class":360},"&lt;template&gt; &lt;div&gt; &lt;h1&gt;系統設定&lt;\u002Fh1&gt; &lt;!-- 只有 admin\n",[60,1891,1892],{"class":62,"line":135},[60,1893,1894],{"class":360},"看得到 --&gt; &lt;\u002Fdiv&gt; &lt;\u002Ftemplate&gt;\n",[46,1896,1898],{"id":1897},"條件顯示-ui-元素","條件顯示 UI 元素",[50,1900,1902],{"className":1870,"code":1901,"language":1872,"meta":55,"style":55},"&lt;script setup lang=\"ts\"&gt; const { user } = useUserSession() const\ncanManageUsers = computed(() =&gt; { return ['admin',\n'manager'].includes(user.value?.role ?? '') }) const canEditSettings =\ncomputed(() =&gt; { return user.value?.role === 'admin' }) &lt;\u002Fscript&gt;\n&lt;template&gt; &lt;UNavigationMenu :items=\"menuItems\"&gt; &lt;!--\n根據權限顯示選單項目 --&gt; &lt;\u002FUNavigationMenu&gt; &lt;UButton\nv-if=\"canManageUsers\" to=\"\u002Fmanage\u002Fusers\"&gt; 使用者管理 &lt;\u002FUButton&gt;\n&lt;UButton v-if=\"canEditSettings\" to=\"\u002Fadmin\u002Fsettings\"&gt; 系統設定\n&lt;\u002FUButton&gt; &lt;\u002Ftemplate&gt;\n",[57,1903,1904,1909,1914,1919,1924,1929,1934,1939,1944],{"__ignoreMap":55},[60,1905,1906],{"class":62,"line":63},[60,1907,1908],{"class":360},"&lt;script setup lang=\"ts\"&gt; const { user } = useUserSession() const\n",[60,1910,1911],{"class":62,"line":70},[60,1912,1913],{"class":360},"canManageUsers = computed(() =&gt; { return ['admin',\n",[60,1915,1916],{"class":62,"line":128},[60,1917,1918],{"class":360},"'manager'].includes(user.value?.role ?? '') }) const canEditSettings =\n",[60,1920,1921],{"class":62,"line":135},[60,1922,1923],{"class":360},"computed(() =&gt; { return user.value?.role === 'admin' }) &lt;\u002Fscript&gt;\n",[60,1925,1926],{"class":62,"line":413},[60,1927,1928],{"class":360},"&lt;template&gt; &lt;UNavigationMenu :items=\"menuItems\"&gt; &lt;!--\n",[60,1930,1931],{"class":62,"line":419},[60,1932,1933],{"class":360},"根據權限顯示選單項目 --&gt; &lt;\u002FUNavigationMenu&gt; &lt;UButton\n",[60,1935,1936],{"class":62,"line":450},[60,1937,1938],{"class":360},"v-if=\"canManageUsers\" to=\"\u002Fmanage\u002Fusers\"&gt; 使用者管理 &lt;\u002FUButton&gt;\n",[60,1940,1941],{"class":62,"line":456},[60,1942,1943],{"class":360},"&lt;UButton v-if=\"canEditSettings\" to=\"\u002Fadmin\u002Fsettings\"&gt; 系統設定\n",[60,1945,1946],{"class":62,"line":462},[60,1947,1948],{"class":360},"&lt;\u002FUButton&gt; &lt;\u002Ftemplate&gt;\n",[40,1950],{},[11,1952,1953],{"id":1953},"動態選單根據角色",[50,1955,1957],{"className":52,"code":1956,"language":54,"meta":55,"style":55},"\u002F\u002F app\u002Fcomposables\u002FuseMenuItems.ts\nimport type { NavigationMenuItem } from '@nuxt\u002Fui'\n\nexport function useMenuItems() {\n  const { user } = useUserSession()\n\n  const menuItems = computed&lt;NavigationMenuItem[]&gt;(() =&gt; {\n    const items: NavigationMenuItem[] = [\n      { label: '儀表板', icon: 'i-lucide-layout-dashboard', to: '\u002F' },\n    ]\n\n    \u002F\u002F staff 以上可見\n    if (hasRole(user.value?.role, 'staff')) {\n      items.push({\n        label: '資料管理',\n        icon: 'i-lucide-database',\n        to: '\u002Fdata',\n      })\n    }\n\n    \u002F\u002F manager 以上可見\n    if (hasRole(user.value?.role, 'manager')) {\n      items.push({\n        label: '使用者管理',\n        icon: 'i-lucide-users',\n        to: '\u002Fmanage\u002Fusers',\n      })\n    }\n\n    \u002F\u002F admin 專屬\n    if (user.value?.role === 'admin') {\n      items.push({\n        label: '系統設定',\n        icon: 'i-lucide-settings',\n        to: '\u002Fadmin\u002Fsettings',\n      })\n    }\n\n    return items\n  })\n\n  return { menuItems }\n}\n",[57,1958,1959,1964,1990,1994,2008,2026,2030,2072,2089,2136,2141,2145,2150,2183,2190,2206,2222,2238,2245,2249,2253,2258,2290,2296,2310,2325,2340,2346,2351,2356,2362,2392,2399,2414,2430,2446,2453,2458,2463,2472,2480,2485,2496],{"__ignoreMap":55},[60,1960,1961],{"class":62,"line":63},[60,1962,1963],{"class":66},"\u002F\u002F app\u002Fcomposables\u002FuseMenuItems.ts\n",[60,1965,1966,1969,1972,1974,1977,1979,1982,1984,1987],{"class":62,"line":70},[60,1967,1968],{"class":349},"import",[60,1970,1971],{"class":349}," type",[60,1973,1112],{"class":124},[60,1975,1976],{"class":360}," NavigationMenuItem",[60,1978,396],{"class":124},[60,1980,1981],{"class":349}," from",[60,1983,427],{"class":85},[60,1985,1986],{"class":89},"@nuxt\u002Fui",[60,1988,1989],{"class":85},"'\n",[60,1991,1992],{"class":62,"line":128},[60,1993,132],{"emptyLinePlaceholder":131},[60,1995,1996,1998,2000,2003,2006],{"class":62,"line":135},[60,1997,350],{"class":349},[60,1999,742],{"class":73},[60,2001,2002],{"class":356}," useMenuItems",[60,2004,2005],{"class":124},"()",[60,2007,676],{"class":124},[60,2009,2010,2013,2015,2017,2019,2021,2024],{"class":62,"line":413},[60,2011,2012],{"class":73},"  const",[60,2014,1112],{"class":124},[60,2016,393],{"class":640},[60,2018,396],{"class":124},[60,2020,82],{"class":81},[60,2022,2023],{"class":356}," useUserSession",[60,2025,406],{"class":446},[60,2027,2028],{"class":62,"line":419},[60,2029,132],{"emptyLinePlaceholder":131},[60,2031,2032,2034,2037,2039,2042,2044,2046,2048,2051,2054,2056,2058,2060,2063,2065,2067,2070],{"class":62,"line":450},[60,2033,2012],{"class":73},[60,2035,2036],{"class":640}," menuItems",[60,2038,82],{"class":81},[60,2040,2041],{"class":360}," computed",[60,2043,650],{"class":81},[60,2045,653],{"class":360},[60,2047,656],{"class":124},[60,2049,2050],{"class":360},"NavigationMenuItem",[60,2052,2053],{"class":446},"[]",[60,2055,650],{"class":81},[60,2057,669],{"class":360},[60,2059,656],{"class":124},[60,2061,2062],{"class":446},"(() ",[60,2064,370],{"class":81},[60,2066,669],{"class":360},[60,2068,2069],{"class":446},"; ",[60,2071,376],{"class":124},[60,2073,2074,2077,2079,2081,2084,2086],{"class":62,"line":456},[60,2075,2076],{"class":446},"    const items",[60,2078,644],{"class":124},[60,2080,1976],{"class":360},[60,2082,2083],{"class":446},"[] ",[60,2085,848],{"class":81},[60,2087,2088],{"class":446}," [\n",[60,2090,2091,2094,2097,2099,2101,2104,2106,2108,2111,2113,2115,2118,2120,2122,2125,2127,2129,2132,2134],{"class":62,"line":462},[60,2092,2093],{"class":124},"      {",[60,2095,2096],{"class":446}," label",[60,2098,644],{"class":124},[60,2100,427],{"class":85},[60,2102,2103],{"class":89},"儀表板",[60,2105,434],{"class":85},[60,2107,390],{"class":124},[60,2109,2110],{"class":446}," icon",[60,2112,644],{"class":124},[60,2114,427],{"class":85},[60,2116,2117],{"class":89},"i-lucide-layout-dashboard",[60,2119,434],{"class":85},[60,2121,390],{"class":124},[60,2123,2124],{"class":446}," to",[60,2126,644],{"class":124},[60,2128,427],{"class":85},[60,2130,2131],{"class":89},"\u002F",[60,2133,434],{"class":85},[60,2135,1140],{"class":124},[60,2137,2138],{"class":62,"line":468},[60,2139,2140],{"class":446},"    ]\n",[60,2142,2143],{"class":62,"line":473},[60,2144,132],{"emptyLinePlaceholder":131},[60,2146,2147],{"class":62,"line":479},[60,2148,2149],{"class":66},"    \u002F\u002F staff 以上可見\n",[60,2151,2152,2155,2157,2159,2161,2163,2165,2167,2169,2171,2173,2175,2177,2179,2181],{"class":62,"line":485},[60,2153,2154],{"class":356},"    if",[60,2156,797],{"class":446},[60,2158,888],{"class":356},[60,2160,361],{"class":446},[60,2162,1662],{"class":360},[60,2164,894],{"class":124},[60,2166,897],{"class":360},[60,2168,900],{"class":124},[60,2170,903],{"class":360},[60,2172,390],{"class":124},[60,2174,427],{"class":85},[60,2176,110],{"class":89},[60,2178,434],{"class":85},[60,2180,914],{"class":446},[60,2182,376],{"class":124},[60,2184,2185,2188],{"class":62,"line":501},[60,2186,2187],{"class":446},"      items.push(",[60,2189,376],{"class":124},[60,2191,2192,2195,2197,2199,2202,2204],{"class":62,"line":506},[60,2193,2194],{"class":77},"        label",[60,2196,644],{"class":124},[60,2198,427],{"class":85},[60,2200,2201],{"class":89},"資料管理",[60,2203,434],{"class":85},[60,2205,690],{"class":124},[60,2207,2208,2211,2213,2215,2218,2220],{"class":62,"line":511},[60,2209,2210],{"class":77},"        icon",[60,2212,644],{"class":124},[60,2214,427],{"class":85},[60,2216,2217],{"class":89},"i-lucide-database",[60,2219,434],{"class":85},[60,2221,690],{"class":124},[60,2223,2224,2227,2229,2231,2234,2236],{"class":62,"line":517},[60,2225,2226],{"class":77},"        to",[60,2228,644],{"class":124},[60,2230,427],{"class":85},[60,2232,2233],{"class":89},"\u002Fdata",[60,2235,434],{"class":85},[60,2237,690],{"class":124},[60,2239,2240,2243],{"class":62,"line":532},[60,2241,2242],{"class":124},"      }",[60,2244,498],{"class":446},[60,2246,2247],{"class":62,"line":545},[60,2248,608],{"class":124},[60,2250,2251],{"class":62,"line":550},[60,2252,132],{"emptyLinePlaceholder":131},[60,2254,2255],{"class":62,"line":555},[60,2256,2257],{"class":66},"    \u002F\u002F manager 以上可見\n",[60,2259,2260,2262,2264,2266,2268,2270,2272,2274,2276,2278,2280,2282,2284,2286,2288],{"class":62,"line":561},[60,2261,2154],{"class":356},[60,2263,797],{"class":446},[60,2265,888],{"class":356},[60,2267,361],{"class":446},[60,2269,1662],{"class":360},[60,2271,894],{"class":124},[60,2273,897],{"class":360},[60,2275,900],{"class":124},[60,2277,903],{"class":360},[60,2279,390],{"class":124},[60,2281,427],{"class":85},[60,2283,101],{"class":89},[60,2285,434],{"class":85},[60,2287,914],{"class":446},[60,2289,376],{"class":124},[60,2291,2292,2294],{"class":62,"line":577},[60,2293,2187],{"class":446},[60,2295,376],{"class":124},[60,2297,2298,2300,2302,2304,2306,2308],{"class":62,"line":591},[60,2299,2194],{"class":77},[60,2301,644],{"class":124},[60,2303,427],{"class":85},[60,2305,254],{"class":89},[60,2307,434],{"class":85},[60,2309,690],{"class":124},[60,2311,2312,2314,2316,2318,2321,2323],{"class":62,"line":605},[60,2313,2210],{"class":77},[60,2315,644],{"class":124},[60,2317,427],{"class":85},[60,2319,2320],{"class":89},"i-lucide-users",[60,2322,434],{"class":85},[60,2324,690],{"class":124},[60,2326,2327,2329,2331,2333,2336,2338],{"class":62,"line":611},[60,2328,2226],{"class":77},[60,2330,644],{"class":124},[60,2332,427],{"class":85},[60,2334,2335],{"class":89},"\u002Fmanage\u002Fusers",[60,2337,434],{"class":85},[60,2339,690],{"class":124},[60,2341,2342,2344],{"class":62,"line":616},[60,2343,2242],{"class":124},[60,2345,498],{"class":446},[60,2347,2349],{"class":62,"line":2348},28,[60,2350,608],{"class":124},[60,2352,2354],{"class":62,"line":2353},29,[60,2355,132],{"emptyLinePlaceholder":131},[60,2357,2359],{"class":62,"line":2358},30,[60,2360,2361],{"class":66},"    \u002F\u002F admin 專屬\n",[60,2363,2365,2367,2369,2371,2373,2375,2377,2379,2382,2384,2386,2388,2390],{"class":62,"line":2364},31,[60,2366,2154],{"class":356},[60,2368,797],{"class":446},[60,2370,1662],{"class":360},[60,2372,894],{"class":124},[60,2374,897],{"class":360},[60,2376,900],{"class":124},[60,2378,903],{"class":360},[60,2380,2381],{"class":81}," ===",[60,2383,427],{"class":85},[60,2385,90],{"class":89},[60,2387,434],{"class":85},[60,2389,806],{"class":446},[60,2391,376],{"class":124},[60,2393,2395,2397],{"class":62,"line":2394},32,[60,2396,2187],{"class":446},[60,2398,376],{"class":124},[60,2400,2402,2404,2406,2408,2410,2412],{"class":62,"line":2401},33,[60,2403,2194],{"class":77},[60,2405,644],{"class":124},[60,2407,427],{"class":85},[60,2409,239],{"class":89},[60,2411,434],{"class":85},[60,2413,690],{"class":124},[60,2415,2417,2419,2421,2423,2426,2428],{"class":62,"line":2416},34,[60,2418,2210],{"class":77},[60,2420,644],{"class":124},[60,2422,427],{"class":85},[60,2424,2425],{"class":89},"i-lucide-settings",[60,2427,434],{"class":85},[60,2429,690],{"class":124},[60,2431,2433,2435,2437,2439,2442,2444],{"class":62,"line":2432},35,[60,2434,2226],{"class":77},[60,2436,644],{"class":124},[60,2438,427],{"class":85},[60,2440,2441],{"class":89},"\u002Fadmin\u002Fsettings",[60,2443,434],{"class":85},[60,2445,690],{"class":124},[60,2447,2449,2451],{"class":62,"line":2448},36,[60,2450,2242],{"class":124},[60,2452,498],{"class":446},[60,2454,2456],{"class":62,"line":2455},37,[60,2457,608],{"class":124},[60,2459,2461],{"class":62,"line":2460},38,[60,2462,132],{"emptyLinePlaceholder":131},[60,2464,2466,2469],{"class":62,"line":2465},39,[60,2467,2468],{"class":360},"    return",[60,2470,2471],{"class":360}," items\n",[60,2473,2475,2478],{"class":62,"line":2474},40,[60,2476,2477],{"class":124},"  }",[60,2479,498],{"class":446},[60,2481,2483],{"class":62,"line":2482},41,[60,2484,132],{"emptyLinePlaceholder":131},[60,2486,2488,2490,2492,2494],{"class":62,"line":2487},42,[60,2489,818],{"class":349},[60,2491,1112],{"class":124},[60,2493,2036],{"class":360},[60,2495,1034],{"class":124},[60,2497,2499],{"class":62,"line":2498},43,[60,2500,731],{"class":124},[40,2502],{},[11,2504,2505],{"id":2505},"踩坑經驗",[46,2507,2509],{"id":2508},"忘記處理-unauthorized-角色","忘記處理 unauthorized 角色",[15,2511,2512,2515],{},[19,2513,2514],{},"問題","：新用戶註冊後可以存取所有需要登入的頁面。",[15,2517,2518,2521,2522,2525],{},[19,2519,2520],{},"原因","：只檢查 ",[57,2523,2524],{},"loggedIn","，沒有檢查角色。",[50,2527,2529],{"className":52,"code":2528,"language":54,"meta":55,"style":55},"\u002F\u002F ❌ 錯誤：只檢查登入狀態\nif (!loggedIn.value) {\n  return navigateTo(\"\u002Flogin\");\n}\n\u002F\u002F 新用戶（unauthorized）會通過檢查\n\n\u002F\u002F ✅ 正確：同時檢查角色\nif (!loggedIn.value) {\n  return navigateTo(\"\u002Flogin\");\n}\nif (user.value?.role === \"unauthorized\") {\n  return navigateTo(\"\u002Fforbidden\");\n}\n",[57,2530,2531,2536,2553,2572,2576,2581,2585,2590,2606,2624,2628,2656,2674],{"__ignoreMap":55},[60,2532,2533],{"class":62,"line":63},[60,2534,2535],{"class":66},"\u002F\u002F ❌ 錯誤：只檢查登入狀態\n",[60,2537,2538,2540,2542,2544,2546,2548,2551],{"class":62,"line":70},[60,2539,883],{"class":349},[60,2541,797],{"class":360},[60,2543,800],{"class":81},[60,2545,2524],{"class":360},[60,2547,894],{"class":124},[60,2549,2550],{"class":360},"value) ",[60,2552,376],{"class":124},[60,2554,2555,2557,2560,2562,2564,2566,2568,2570],{"class":62,"line":128},[60,2556,818],{"class":349},[60,2558,2559],{"class":356}," navigateTo",[60,2561,361],{"class":446},[60,2563,93],{"class":85},[60,2565,493],{"class":89},[60,2567,93],{"class":85},[60,2569,782],{"class":446},[60,2571,125],{"class":124},[60,2573,2574],{"class":62,"line":135},[60,2575,731],{"class":124},[60,2577,2578],{"class":62,"line":413},[60,2579,2580],{"class":66},"\u002F\u002F 新用戶（unauthorized）會通過檢查\n",[60,2582,2583],{"class":62,"line":419},[60,2584,132],{"emptyLinePlaceholder":131},[60,2586,2587],{"class":62,"line":450},[60,2588,2589],{"class":66},"\u002F\u002F ✅ 正確：同時檢查角色\n",[60,2591,2592,2594,2596,2598,2600,2602,2604],{"class":62,"line":456},[60,2593,883],{"class":349},[60,2595,797],{"class":360},[60,2597,800],{"class":81},[60,2599,2524],{"class":360},[60,2601,894],{"class":124},[60,2603,2550],{"class":360},[60,2605,376],{"class":124},[60,2607,2608,2610,2612,2614,2616,2618,2620,2622],{"class":62,"line":462},[60,2609,818],{"class":349},[60,2611,2559],{"class":356},[60,2613,361],{"class":446},[60,2615,93],{"class":85},[60,2617,493],{"class":89},[60,2619,93],{"class":85},[60,2621,782],{"class":446},[60,2623,125],{"class":124},[60,2625,2626],{"class":62,"line":468},[60,2627,731],{"class":124},[60,2629,2630,2632,2635,2637,2639,2641,2644,2646,2648,2650,2652,2654],{"class":62,"line":473},[60,2631,883],{"class":349},[60,2633,2634],{"class":360}," (user",[60,2636,894],{"class":124},[60,2638,897],{"class":360},[60,2640,900],{"class":124},[60,2642,2643],{"class":360},"role ",[60,2645,1392],{"class":81},[60,2647,86],{"class":85},[60,2649,119],{"class":89},[60,2651,93],{"class":85},[60,2653,806],{"class":360},[60,2655,376],{"class":124},[60,2657,2658,2660,2662,2664,2666,2668,2670,2672],{"class":62,"line":479},[60,2659,818],{"class":349},[60,2661,2559],{"class":356},[60,2663,361],{"class":446},[60,2665,93],{"class":85},[60,2667,441],{"class":89},[60,2669,93],{"class":85},[60,2671,782],{"class":446},[60,2673,125],{"class":124},[60,2675,2676],{"class":62,"line":485},[60,2677,731],{"class":124},[46,2679,2681],{"id":2680},"只在-client-端檢查權限","只在 Client 端檢查權限",[15,2683,2684,2686],{},[19,2685,2514],{},"：API 被直接呼叫時沒有權限保護。",[15,2688,2689,2692],{},[19,2690,2691],{},"解決","：Server API 必須獨立驗證權限。",[50,2694,2696],{"className":52,"code":2695,"language":54,"meta":55,"style":55},"\u002F\u002F ✅ Server 端獨立檢查\nexport default defineEventHandler(async (event) =&gt; {\n  const { user } = await requireUserSession(event, {\n    user: { role: ['admin', 'manager'] },\n  })\n  \u002F\u002F 即使 Client 繞過，Server 也會擋住\n})\n",[57,2697,2698,2703,2723,2745,2777,2781,2786],{"__ignoreMap":55},[60,2699,2700],{"class":62,"line":63},[60,2701,2702],{"class":66},"\u002F\u002F ✅ Server 端獨立檢查\n",[60,2704,2705,2707,2709,2711,2713,2715,2717,2719,2721],{"class":62,"line":70},[60,2706,350],{"class":349},[60,2708,353],{"class":349},[60,2710,953],{"class":356},[60,2712,361],{"class":360},[60,2714,364],{"class":356},[60,2716,960],{"class":360},[60,2718,370],{"class":81},[60,2720,373],{"class":360},[60,2722,376],{"class":124},[60,2724,2725,2727,2729,2731,2733,2735,2737,2739,2741,2743],{"class":62,"line":128},[60,2726,381],{"class":360},[60,2728,384],{"class":124},[60,2730,393],{"class":360},[60,2732,396],{"class":124},[60,2734,984],{"class":360},[60,2736,987],{"class":402},[60,2738,361],{"class":124},[60,2740,992],{"class":753},[60,2742,390],{"class":124},[60,2744,676],{"class":124},[60,2746,2747,2749,2751,2753,2755,2757,2759,2761,2763,2765,2767,2769,2771,2773,2775],{"class":62,"line":135},[60,2748,1107],{"class":1106},[60,2750,644],{"class":124},[60,2752,1112],{"class":124},[60,2754,1115],{"class":1106},[60,2756,644],{"class":124},[60,2758,1120],{"class":124},[60,2760,434],{"class":85},[60,2762,90],{"class":89},[60,2764,434],{"class":85},[60,2766,390],{"class":124},[60,2768,427],{"class":85},[60,2770,101],{"class":89},[60,2772,434],{"class":85},[60,2774,1137],{"class":124},[60,2776,1140],{"class":124},[60,2778,2779],{"class":62,"line":413},[60,2780,1145],{"class":124},[60,2782,2783],{"class":62,"line":419},[60,2784,2785],{"class":66},"  \u002F\u002F 即使 Client 繞過，Server 也會擋住\n",[60,2787,2788,2790],{"class":62,"line":450},[60,2789,1039],{"class":124},[60,2791,498],{"class":360},[46,2793,2794],{"id":2794},"權限檢查散落各處",[15,2796,2797,2799],{},[19,2798,2514],{},"：權限邏輯分散在多個檔案，難以維護。",[15,2801,2802,2804],{},[19,2803,2691],{},"：建立統一的權限檢查工具。",[50,2806,2808],{"className":52,"code":2807,"language":54,"meta":55,"style":55},"\u002F\u002F app\u002Futils\u002Fpermission.ts - 集中管理\nexport const PERMISSIONS = {\n  MANAGE_USERS: [\"admin\", \"manager\"],\n  EDIT_SETTINGS: [\"admin\"],\n  VIEW_DATA: [\"admin\", \"manager\", \"staff\"],\n} as const;\n\nexport function can(\n  userRole: string | undefined,\n  permission: keyof typeof PERMISSIONS,\n): boolean {\n  if (!userRole) return false;\n  return PERMISSIONS[permission].includes(userRole as any);\n}\n",[57,2809,2810,2815,2829,2856,2875,2910,2921,2925,2936,2950,2967,2977,2996,3026],{"__ignoreMap":55},[60,2811,2812],{"class":62,"line":63},[60,2813,2814],{"class":66},"\u002F\u002F app\u002Futils\u002Fpermission.ts - 集中管理\n",[60,2816,2817,2819,2822,2825,2827],{"class":62,"line":70},[60,2818,350],{"class":349},[60,2820,2821],{"class":73}," const",[60,2823,2824],{"class":640}," PERMISSIONS",[60,2826,82],{"class":81},[60,2828,676],{"class":124},[60,2830,2831,2834,2836,2838,2840,2842,2844,2846,2848,2850,2852,2854],{"class":62,"line":128},[60,2832,2833],{"class":446},"  MANAGE_USERS",[60,2835,644],{"class":124},[60,2837,1120],{"class":360},[60,2839,93],{"class":85},[60,2841,90],{"class":89},[60,2843,93],{"class":85},[60,2845,390],{"class":124},[60,2847,86],{"class":85},[60,2849,101],{"class":89},[60,2851,93],{"class":85},[60,2853,1137],{"class":360},[60,2855,690],{"class":124},[60,2857,2858,2861,2863,2865,2867,2869,2871,2873],{"class":62,"line":135},[60,2859,2860],{"class":446},"  EDIT_SETTINGS",[60,2862,644],{"class":124},[60,2864,1120],{"class":360},[60,2866,93],{"class":85},[60,2868,90],{"class":89},[60,2870,93],{"class":85},[60,2872,1137],{"class":360},[60,2874,690],{"class":124},[60,2876,2877,2880,2882,2884,2886,2888,2890,2892,2894,2896,2898,2900,2902,2904,2906,2908],{"class":62,"line":413},[60,2878,2879],{"class":446},"  VIEW_DATA",[60,2881,644],{"class":124},[60,2883,1120],{"class":360},[60,2885,93],{"class":85},[60,2887,90],{"class":89},[60,2889,93],{"class":85},[60,2891,390],{"class":124},[60,2893,86],{"class":85},[60,2895,101],{"class":89},[60,2897,93],{"class":85},[60,2899,390],{"class":124},[60,2901,86],{"class":85},[60,2903,110],{"class":89},[60,2905,93],{"class":85},[60,2907,1137],{"class":360},[60,2909,690],{"class":124},[60,2911,2912,2914,2917,2919],{"class":62,"line":419},[60,2913,1039],{"class":124},[60,2915,2916],{"class":349}," as",[60,2918,2821],{"class":73},[60,2920,125],{"class":124},[60,2922,2923],{"class":62,"line":450},[60,2924,132],{"emptyLinePlaceholder":131},[60,2926,2927,2929,2931,2934],{"class":62,"line":456},[60,2928,350],{"class":349},[60,2930,742],{"class":73},[60,2932,2933],{"class":356}," can",[60,2935,748],{"class":124},[60,2937,2938,2940,2942,2944,2946,2948],{"class":62,"line":462},[60,2939,754],{"class":753},[60,2941,644],{"class":81},[60,2943,760],{"class":759},[60,2945,96],{"class":81},[60,2947,765],{"class":759},[60,2949,690],{"class":124},[60,2951,2952,2955,2957,2960,2963,2965],{"class":62,"line":468},[60,2953,2954],{"class":753},"  permission",[60,2956,644],{"class":81},[60,2958,2959],{"class":81}," keyof",[60,2961,2962],{"class":81}," typeof",[60,2964,2824],{"class":640},[60,2966,690],{"class":124},[60,2968,2969,2971,2973,2975],{"class":62,"line":473},[60,2970,782],{"class":124},[60,2972,644],{"class":81},[60,2974,787],{"class":759},[60,2976,676],{"class":124},[60,2978,2979,2981,2983,2985,2987,2989,2991,2994],{"class":62,"line":479},[60,2980,794],{"class":349},[60,2982,797],{"class":446},[60,2984,800],{"class":81},[60,2986,803],{"class":360},[60,2988,806],{"class":446},[60,2990,809],{"class":349},[60,2992,2993],{"class":812}," false",[60,2995,125],{"class":124},[60,2997,2998,3000,3002,3004,3007,3009,3011,3013,3015,3017,3019,3022,3024],{"class":62,"line":485},[60,2999,818],{"class":349},[60,3001,2824],{"class":640},[60,3003,826],{"class":446},[60,3005,3006],{"class":360},"permission",[60,3008,1137],{"class":446},[60,3010,894],{"class":124},[60,3012,1429],{"class":356},[60,3014,361],{"class":446},[60,3016,803],{"class":360},[60,3018,2916],{"class":349},[60,3020,3021],{"class":759}," any",[60,3023,782],{"class":446},[60,3025,125],{"class":124},[60,3027,3028],{"class":62,"line":501},[60,3029,731],{"class":124},[40,3031],{},[11,3033,3034],{"id":3034},"最佳實踐總結",[3036,3037,3038,3044,3050,3056,3066],"ol",{},[27,3039,3040,3043],{},[19,3041,3042],{},"雙重檢查","：Client 端 + Server 端都要檢查權限",[27,3045,3046,3049],{},[19,3047,3048],{},"預設拒絕","：未定義權限的路由預設拒絕存取",[27,3051,3052,3055],{},[19,3053,3054],{},"角色階層","：使用數值比較實現角色繼承",[27,3057,3058,3061,3062,3065],{},[19,3059,3060],{},"集中管理","：權限邏輯放在 ",[57,3063,3064],{},"utils\u002Fpermission.ts"," 統一維護",[27,3067,3068,3071],{},[19,3069,3070],{},"預設 unauthorized","：新用戶預設無權限，需管理員授權",[40,3073],{},[11,3075,3076],{"id":3076},"延伸閱讀",[24,3078,3079,3088,3095,3102],{},[27,3080,3081],{},[3082,3083,3087],"a",{"href":3084,"rel":3085},"https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fguide\u002Fdirectory-structure\u002Fmiddleware",[3086],"nofollow","Nuxt Middleware 文件",[27,3089,3090],{},[3082,3091,3094],{"href":3092,"rel":3093},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FRole-based_access_control",[3086],"RBAC 設計模式",[27,3096,3097,3098],{},"上一篇：",[3082,3099,3101],{"href":3100},"\u002Fblog\u002Fnuxt\u002Fbetter-auth-integration","nuxt-better-auth 認證整合",[27,3103,3104,3105],{},"下一篇：",[3082,3106,3108],{"href":3107},"\u002Fblog\u002Fnuxt\u002Fsupabase-local-first","Supabase Local-First 開發流程",[3110,3111,3112],"style",{},"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 .sbsja, html code.shiki .sbsja{--shiki-light:#9C3EDA;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sbgvK, html code.shiki .sbgvK{--shiki-light:#E2931D;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .smGrS, html code.shiki .smGrS{--shiki-light:#39ADB5;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sjJ54, html code.shiki .sjJ54{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s_sjI, html code.shiki .s_sjI{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sP7_E, html code.shiki .sP7_E{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#E1E4E8}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 .sVXei, html code.shiki .sVXei{--shiki-light:#E53935;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZUrc, html code.shiki .sZUrc{--shiki-light:#E53935;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .skxfh, html code.shiki .skxfh{--shiki-light:#E53935;--shiki-default:#24292E;--shiki-dark:#E1E4E8}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 .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 .sZMiF, html code.shiki .sZMiF{--shiki-light:#E2931D;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .syTEX, html code.shiki .syTEX{--shiki-light:#FF5370;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sucvu, html code.shiki .sucvu{--shiki-light:#E53935;--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":55,"searchDepth":128,"depth":128,"links":3114},[3115,3116,3121,3122,3126,3131,3135,3139,3140,3145,3146],{"id":13,"depth":70,"text":13},{"id":44,"depth":70,"text":44,"children":3117},[3118,3119,3120],{"id":48,"depth":128,"text":48},{"id":141,"depth":128,"text":141},{"id":215,"depth":128,"text":215},{"id":306,"depth":70,"text":306},{"id":329,"depth":70,"text":330,"children":3123},[3124,3125],{"id":333,"depth":128,"text":334},{"id":622,"depth":128,"text":622},{"id":930,"depth":70,"text":931,"children":3127},[3128,3129,3130],{"id":934,"depth":128,"text":934},{"id":1044,"depth":128,"text":1044},{"id":1208,"depth":128,"text":1208},{"id":1543,"depth":70,"text":1544,"children":3132},[3133,3134],{"id":1547,"depth":128,"text":1548},{"id":1804,"depth":128,"text":1804},{"id":1863,"depth":70,"text":1863,"children":3136},[3137,3138],{"id":1866,"depth":128,"text":1867},{"id":1897,"depth":128,"text":1898},{"id":1953,"depth":70,"text":1953},{"id":2505,"depth":70,"text":2505,"children":3141},[3142,3143,3144],{"id":2508,"depth":128,"text":2509},{"id":2680,"depth":128,"text":2681},{"id":2794,"depth":128,"text":2794},{"id":3034,"depth":70,"text":3034},{"id":3076,"depth":70,"text":3076},"Nuxt","2026-01-22","實作 RBAC 角色權限控制，涵蓋 Client 端路由守衛、Server 端權限檢查、與 nuxt.config 的 routeRules 設定。",false,"md",null,{},"\u002Fblog\u002Fnuxt\u002Frole-based-access-control",{"title":5,"description":3149},"nuxt-fullstack","Nuxt 4 全棧實戰筆記","blog\u002Fnuxt\u002Frole-based-access-control\u002Findex",[3147,3160,3161],"Authentication","OAuth","EXGK_BOn3IEJbh3MBssx5bkNewuNNCrvYG59f5apl04",1780512499430]