iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
Vibe Coding

《我與 AI 的奇幻漂流:30 天,把「能跑」變成「能上線」》系列 第 21 篇

【Day 21|瞭望】管理員後台與角色權限:登入只是第一關

  • 分享至 

  • xImage
  •  

Day 17 做好回報功能之後,每一則回報都會透過 Telegram 通知我。

一開始很方便,手機一響就知道有人回報。但後續如果回報一多,問題就來了:哪些已經修正?哪些還沒看?哪些是重複的?Telegram 只會一直響,卻沒有地方能記錄處理進度。想知道一筆回報處理到哪裡,只能自己另外記,或是直接進資料庫修改。

漂流的時候,Pi 每天都在看海平面,留意遠方有沒有船、天氣會不會變。網站也需要一個這樣的瞭望台:一眼就能看見所有回報,並更新處理進度。所以今天要來做管理員後台。

今天用到的觀念,大部分前幾天都出現過:Day 16 的繞過畫面送出請求、Day 17 的不信任輸入、Day 18 的身分、Day 19 的 RLS、Day 20 的渲染。

不同的是,Day 19 的規則是「每個人只能碰自己的」;今天第一次出現「有一種人,可以看見所有人送來的資料」。這種權限要怎麼給、給到哪裡為止,是今天的新問題。

這也是這個系列想做的事:比起照著步驟做出某個功能,更想把網站背後的底層邏輯拆清楚。功能會一直變,但這些觀念會不斷被組合使用。


一、後台要做什麼?

我先請 Codex 列出回報管理後台可以有哪些功能。它列得很完整:指派處理人員、多人協作、內部留言、通知中心、權限分級……

但目前只有我一個管理員。我需要的是一個能追蹤回報的地方,不是一套企業級的客服工單系統。所以目前先只做:

功能 說明
回報列表 時間、專輯、版本、回報內容、目前狀態
狀態 未處理 → 處理中 → 已修正/不處理
備註 寫給自己看的處理紀錄
結案資訊 最後的處理者、結案時間

有趣的是,Codex 盤點資料表時發現 Day 17 建立回報表時,就已經設計好這四種狀態,連「已修正和不處理一定要有結案時間」的規則都在。這次只需要再加上備註和處理者兩個欄位。

要注意的是,這只記錄每筆回報目前的狀態和最後的結案資訊,不是完整的操作歷史。如果一筆回報被改了好幾次狀態,中間的過程不會留下來。我的想法是目前只有一位管理員,這樣暫時就夠了;等真的需要多人協作,或是發現過程是有必要記錄下來的話,可以再考慮另外建立一張異動紀錄表。


二、多一個 /admin 就安全了嗎?

最直覺的做法,是新增一個 /admin 頁面,然後不要把連結放在導覽列上。但 Day 16 我們就看過:網址藏起來,不代表別人猜不到;就算猜不到,只要知道 Data API 網址和公開金鑰,也可以繞過網站直接發請求。

所以後台需要三道門:

做什麼 擋住誰
第一道:路由 不是管理員,打開 /admin 就顯示 404 一般使用者誤闖(這是呈現方式,不是保護)
第二道:伺服器 每次讀取或修改回報時,在伺服器確認登入和管理員身分 直接打開後台網址、偽造表單請求的人
第三道:資料庫 就算有人不經過網站,資料庫也只讓管理員讀回報、只讓管理員用指定的方式修改 繞過網站、直接打 API 的人

https://ithelp.ithome.com.tw/upload/images/20261005/20178017DXXrQjUgqp.png
【圖1|三道門防禦】

第一道主要是呈現方式;第二道在應用層確認這個請求有沒有資格執行;第三道則是資料層最後的防線。即使前面的程式寫錯,資料庫仍然不應該放行。這也是 Day 18 那句話:前端擋不住,後端才是真的門;而資料庫,是最後一道門。

每一道門都假設前一道可能失守,這種做法叫做縱深防禦(Defense in Depth)。

為什麼顯示 404,而不是「沒有權限」?

「沒有權限」等於告訴對方:這裡有東西,只是你進不來。
404 則是選擇不公開後台的存在,對一般使用者來說,後台本來就不需要出現。

但這只是呈現方式,不是額外的保護。真正擋住資料的,還是後面兩道門。


三、誰是管理員?

網站要怎麼知道誰是管理員?常見的做法有三種:

做法 怎麼判斷 優點 限制
環境變數寫死 伺服器比對 ADMIN_EMAIL 最簡單 資料庫看不到環境變數,只能靠程式擋;換人要重新部署
管理員資料表 建一張 admins 表,資料庫檢查目前使用者在不在裡面 規則寫在資料庫,繞過網站也擋得住;改了立刻生效 管理員表本身也要上鎖
寫進登入通行證 在使用者的 app_metadata 標上管理員角色 不用每次查表 改了角色,要等通行證更新才生效

這次選擇管理員資料表。規則寫在資料庫裡,而且新增或移除管理員,下一次請求就生效。

user_metadata 不能拿來判斷權限

Supabase 的使用者有兩種額外資料:user_metadata 和 app_metadata。user_metadata 是使用者自己就改得到的,例如暱稱、大頭貼。如果用它來判斷誰是管理員,任何人都能把自己升級成管理員。

審 AI 寫的權限規則時,這是值得特別看一眼的地方。

3.1 第一個管理員怎麼來?

管理員表建議不要讓網站上的任何人新增,最好連管理員自己也不行。否則只要找到一個漏洞,就能把自己加進去。那第一個管理員怎麼來?答案是:不經過網站。由我在 Supabase 後台的 SQL 編輯器(SQL Editor)手動加入自己。

insert into public.admins (user_id)
values ('我的使用者 id');

網站上,沒有任何一個按鈕可以讓人變成管理員。

如果不只一種管理員呢?

admins 表只回答一個問題:你是不是管理員。如果未來有不同分工,例如有人只能處理回報、有人可以修改專輯資料,就需要把「角色」和「權限」分開設計,這叫做角色權限控管(RBAC,Role-Based Access Control)。

專案中目前只有一位管理員,一張表就夠了。等真的出現第二種分工,再升級也不遲。


四、讀取靠 RLS 和欄位權限,修改只走一扇門

Day 19 的規則是「每個人只能碰自己的收藏」。今天的規則是:

身分 讀取回報 修改回報 刪除回報
訪客、一般使用者 不行 不行 不行
管理員 可以讀全部 只能改狀態和備註 暫定不行

4.1 讀取:RLS 管列,欄位權限管欄

先寫一個判斷函式,讀取規則和修改函式,都用它來判斷是不是管理員:

create function public.is_admin()
returns boolean
language sql
stable
security definer
set search_path = ''
as $$
  select exists (
    select 1 from public.admins
    where user_id = (select auth.uid())
  );
$$;

create policy "Admins can read data reports"
  on public.data_reports for select to authenticated
  using ((select public.is_admin()));

security definer 的意思是:這個函式用建立者的權限去查 admins 表。之所以需要這樣,是因為一般使用者本來就沒有權限直接讀 admins 表,但 RLS 又需要知道「你是不是管理員」。

RLS 決定管理員能讀哪幾筆回報。但回報裡有一個欄位,是 Day 17 用來限流的訪客指紋,後台用不到,也不該讓管理員看到。RLS 是以「列」為單位,管不到同一筆資料裡的哪幾個欄位。

這時候要用另一道鎖:欄位層級的權限。

-- 只能讀這些欄位(不包含訪客指紋)
grant select (id, album_id, version_id, message, status,
              created_at, resolved_at, admin_note, handled_by)
  on public.data_reports to authenticated;
鎖 管什麼
RLS 能碰哪幾列(哪幾筆回報)
欄位權限 能碰哪幾欄(回報裡的哪些欄位)

4.2 修改:這次只開一扇門

管理員要能改狀態和備註;處理者和結案時間,我希望由系統自動記錄,不讓人隨意填。(也對處理者比較省事)

如果只在 Server Action 做限制,會有一個缺口:管理員手上有自己的通行證,只要資料表開放他修改這些欄位,他就能不經過 Server Action,直接呼叫 Data API 填入任意值。

所以這次我選擇讓管理員沒有資料表的修改權限,只能透過一個資料庫函式修改。這個函式只接受三個值:哪一筆、新狀態、備註;處理者和結案時間,由資料庫自動填入。

限制放在哪裡 能擋住什麼
前端、Server Action 一般操作、偽造的表單請求
資料庫函式 不走網站流程、直接打 Data API 的人

這不是唯一的做法。如果處理者可以讓人自己填,只用欄位權限就夠了;如果是傳統的網站架構,資料庫本來就不對外開放,由後端程式把關也很常見。選哪一種,要看使用者能不能直接碰到資料庫,以及哪些欄位不能讓人亂填。

資料表上鎖了,函式呢?

PostgreSQL 函式本身也有 EXECUTE 權限,而且新函式通常會帶有對 PUBLIC 的執行權限,所以不能只鎖資料表,還要另外確認誰能呼叫這個函式。
所以這個函式只開放給登入的使用者,函式裡再檢查一次 is_admin()。因為登入的使用者,不等於管理員。

刪除則是一條規則都沒寫、一個權限都沒給,這樣下來,一般網站角色不管從網站還是 Data API,都沒有刪除回報的能力。

在這樣的實作下,管理員並不是擁有一切權限的人,而是被明確授予特定操作能力的人。只給完成工作所需的權限,這叫做最小權限原則(Principle of Least Privilege),管理員也一樣適用。

4.3 後台不用 secret key

Day 17 送出回報時,我們選擇由伺服器先驗證內容、限流,再用 secret key 寫入。這是當時的設計選擇:訪客沒有登入,由伺服器統一把關。

那後台讀回報,也用 secret key 不是比較簡單嗎?

用管理員自己的通行證 用 secret key
RLS 每次都會檢查 直接略過
程式寫錯時 資料庫還會擋 可能把全部回報交給錯的人
資料庫知道是誰操作 知道 只知道是「伺服器」

這次選擇用管理員自己的通行證。多一層保護,就算後台程式哪裡寫錯了,資料庫也不會把回報交給不是管理員的人。

secret key 繼續只用在原本的地方:接收訪客回報、更新 Telegram 通知狀態。


五、後台在哪裡渲染?

Day 20 學了幾種渲染方式,今天直接拿來用:

部分 在哪裡做 原因
確認管理員身分 伺服器 不能交給瀏覽器判斷
取得回報列表 伺服器 確認身分後才拿資料
依狀態篩選 瀏覽器 回報不多,用 Day 20 的方法 A 就好
修改狀態和備註 伺服器(Server Action)呼叫資料庫函式 處理者和結案時間由資料庫填入,不相信任何傳進來的值

這次選擇在伺服器確認管理員身分、取得回報。後台內容包含受保護資料,不適合做成可公開共用的快取內容。

後台也有自己的版面(app/admin/layout.tsx),負責共同介面和初步的身分檢查,不會影響首頁、專輯頁這些公開頁面。

不過,layout 不能是唯一的檢查。Next.js 在站內換頁時,可能會重複使用同一個 layout,不一定每次都重新執行。所以真正讀取回報的頁面、修改狀態的 Server Action 都會各自確認身分,最後資料庫還會再檢查一次。不能因為某個人曾經進入後台,就認定他之後的每一個請求都有管理權限。

等回報變多,再照 Day 20 的判斷流程,改成分頁加上資料庫查詢。

回報內容,要當成不可信任的資料

回報是訪客寫的,裡面可能放了任何東西,包括 HTML 或程式碼。React 預設會把文字當成純文字顯示,但只要使用了 dangerouslySetInnerHTML 這類寫法,就可能讓別人的程式在管理員的瀏覽器裡執行。

Day 17 驗證「送進來的資料能不能信」,今天則是「顯示出來的資料能不能信」。


六、實作與驗收

經過與 AI 的多輪溝通與實作,成功實作了管理員後台(目前管理員後台只先處理回報問題,新增/編輯專輯資料之後再加)

https://ithelp.ithome.com.tw/upload/images/20261005/20178017Rlb2Y6zv4N.png
【圖2|以管理員身份登入時,頂部 Nav 自動顯示回報管理入口】

https://ithelp.ithome.com.tw/upload/images/20261005/20178017kvbDJHrHH1.png
【圖3|可成功進入管理員頁面】

https://ithelp.ithome.com.tw/upload/images/20261005/20178017GK4lPPFxm2.png
【圖4|非管理員無法進入管理員頁面】

Day 19 時,我們防止 A 看到 B 的收藏;今天,實作完後則是要確認一般使用者沒有辦法進入管理員頁面。

所以驗收要分四種身分(未登入訪客、已登入一般使用者、已登入管理員、被移除的管理員),用各自真正的登入狀態測試。在 Supabase 的 SQL 編輯器裡測不準,因為那裡的身分會直接略過 RLS。

身分 測試 預期結果
訪客 用 Day 17 的回報表單送出 照樣可以送出
訪客 打開 /admin 404
一般使用者 打開 /admin 404
一般使用者 繞過網站,直接向 Supabase 讀回報 拿不到任何資料
一般使用者 直接修改回報狀態 沒有任何一筆被修改
一般使用者 把自己加進 admins 被拒絕
一般使用者 直接呼叫修改函式 被拒絕,回報不變
管理員 打開 /admin 看到回報列表
管理員 修改狀態和備註,重新整理 修改保留下來
管理員 嘗試修改回報的原始內容 被拒絕
管理員 繞過 Server Action,直接修改處理者或結案時間 被拒絕
管理員 嘗試刪除回報 被拒絕
管理員 在 SQL 編輯器把自己從 admins 移除,再重新整理 立刻變成 404
被移除的管理員 用原本還沒過期的通行證呼叫修改函式 被拒絕,不用等通行證過期
管理員 登出後重新整理 /admin 404

其中最重要的是「一般使用者直接向 Supabase 讀回報」和「直接呼叫修改函式」這兩列:前兩道門擋住的是畫面,這兩列驗證的才是資料。 最後一列則證明第三節說的「改了立刻生效」,不是說說而已。


七、延伸思考:回報要不要實名?

目前的回報是匿名的,要不要改成登入才能回報?

做法 優點 代價
完全匿名(現在) 門檻最低,看到錯就能回報 無法通知回報者處理結果;要另外設計防濫用措施,目前靠限流
一定要登入 能依帳號限流,也能有「我的回報」 門檻變高,只是想回報一個錯字,卻得先登入
匿名為主,登入者可選擇關聯帳號 保留低門檻,也能讓登入者追蹤進度 資料設計和隱私處理比較多

如果未來要做第三種,可以在回報表加一個可以空白的 user_id,由伺服器根據登入狀態填入,而不是相信前端傳來的 id;登入者也可以透過 RLS,只看到自己的回報。

不過,在決定要不要多存一個欄位之前,可以先問幾個問題:

我們真的需要知道是誰回報的嗎?還是只需要讓回報者知道,這件事處理到哪裡了?

這兩件事不一定要綁在一起。例如送出回報後給一組追蹤碼,回報者不用登入,也能查詢處理進度。不過追蹤碼必須夠難猜,不能用流水號,查詢結果也不能透露回報者的其他資訊。

資料要不要多存一個欄位,不只是工程問題,也是產品和隱私的決定。這次先維持匿名,等真的被濫用,或真的需要通知回報者時,再回來決定。

可以呼應這個系列文中很常提到的一件事:一個功能的實作方法絕對不唯一,取決於你想要做到什麼程度、需要有哪些附加功能。我通常會在與 AI 溝通的過程中去說明我想要的一些功能細節,如此一來答案會比較容易在較少次與 AI 的來回對話中取得。

如果之後想讓管理員也能修改專輯資料,就可以不用從零開始:要開放哪些欄位?用欄位權限,還是資料庫函式?改錯了能不能復原?前面學過的觀念,就是思考這些問題的起點。再搭配 AI 進行規劃,可以讓開發變得更加順暢。


換個領域:其他產品的管理員

產品 一般使用者 管理者
電商網站 只看到自己的訂單和出貨進度 看到所有訂單、更新出貨狀態、上架商品
客服系統 只看到自己的工單 看到所有工單、可以改狀態
線上課程 只看到自己的作業成績 老師看到全班的作業
社群平台 只能檢舉內容 版主看到所有檢舉、決定處理方式
協作文件 只能看或留言 擁有者決定誰能看、誰能編輯

有些產品可能還會有一個特點:管理者可能不只一種,以電商為例:

角色 可以做的 通常不需要的
客服 查訂單、更新出貨狀態 修改商品價格
商品管理 上架商品、修改價格和庫存 看到客人的地址、電話
財務 看營收報表、處理退款 修改商品內容

這就是第三節提到的角色權限控管(RBAC),也是最小權限原則的實際樣子:每種角色只拿完成工作需要的權限。


結語與明日預告

今天,網站多了一個能看見全部回報的地方。後台重要的不只是畫面,而是權限的控管:路由讓一般使用者不會誤闖,伺服器確認身分,資料庫做最後的把關。權限不只是讓該進來的人進來,更要讓不該進來的人拿不到資料。

回頭看,今天新學到的其實只有幾件事,其他都是前幾天觀念的組合:

新學到的 一句話
欄位權限 RLS 管列,欄位權限管欄
資料庫函式 修改只走一扇門,函式也要管誰能呼叫
最小權限 管理員也只拿需要的權限
縱深防禦 每一道門,都假設前一道會失守

不過,後台現在只靠一組帳號保護。如果管理員的帳號被盜,所有回報都看得到。這件事,我們留到後續與資安相關的文章再來處理。

明天,我們回到使用者這一邊。逛了很多專輯之後,想回頭找剛剛看過的那一張,卻怎麼也想不起名字。網站能不能幫我記住,我剛剛去過哪裡?

而「記住」這件事,其實網站一直都在做,只是我們沒注意到。

我們明天見。


上一篇
【Day 20|捕魚的結果】網頁在哪裡被做出來?渲染方式、搜尋與資料狀態
下一篇
【Day 22|航跡】最近瀏覽與快取:資料一定要每次去資料庫查嗎?
系列文
《我與 AI 的奇幻漂流:30 天,把「能跑」變成「能上線」》 共 22 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言