{"allowContribute":false,"item":{"content":"\u003e **日期**：2026-07-05  \n\u003e **分支**：`cubelv-cli-announcement-hardening-v2`（尚未 push）  \n\u003e **狀態**：僅完成程式碼與 SQL 草案輸出，尚未修改任何正式系統。\n\n* * *\n\n## A. index.html 修改摘要\n\n\u003ctable style=\"min-width: 50px;\"\u003e\u003ccolgroup\u003e\u003ccol style=\"min-width: 25px;\"\u003e\u003ccol style=\"min-width: 25px;\"\u003e\u003c/colgroup\u003e\u003ctbody\u003e\u003ctr\u003e\u003cth colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e原來 ❌\u003c/p\u003e\u003c/th\u003e\u003cth colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e改後 ✅\u003c/p\u003e\u003c/th\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e從 \u003ccode\u003elocalStorage\u003c/code\u003e 讀 Supabase 設定\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e硬編碼 \u003ccode\u003eSUPABASE_URL\u003c/code\u003e、\u003ccode\u003eSUPABASE_PUBLISHABLE_KEY\u003c/code\u003e 常數\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e無 localStorage → 顯示錯誤\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e頁面載入自動初始化，直接讀公告\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003eloadPublishedAnnouncements(url, key)\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e無參數版本\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/tbody\u003e\u003c/table\u003e\n\n關鍵程式碼：\n\n```js\nconst SUPABASE_URL = 'https://YOUR-PROJECT.supabase.co';\nconst SUPABASE_PUBLISHABLE_KEY = 'sb_publishable_YOUR_KEY';\n\n(function init() {\n  if (!SUPABASE_URL.includes('YOUR-PROJECT') \u0026\u0026 !SUPABASE_PUBLISHABLE_KEY.includes('YOUR_KEY')) {\n    loadPublishedAnnouncements();\n  }\n})();\n```\n\n*   ✅ 不依賴 localStorage\n    \n*   ✅ 只讀取 `status = 'published'` 的公告\n    \n*   ✅ 排序：`is_pinned` DESC → `published_at` DESC\n    \n*   ✅ 無公告時顯示「目前尚無已發布公告。」\n    \n*   ✅ 絕不使用 service_role key / secret key\n    \n\n* * *\n\n## B. announcement-admin.html 修改摘要\n\n\u003ctable style=\"min-width: 50px;\"\u003e\u003ccolgroup\u003e\u003ccol style=\"min-width: 25px;\"\u003e\u003ccol style=\"min-width: 25px;\"\u003e\u003c/colgroup\u003e\u003ctbody\u003e\u003ctr\u003e\u003cth colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e原來 ❌\u003c/p\u003e\u003c/th\u003e\u003cth colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e改後 ✅\u003c/p\u003e\u003c/th\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e手動輸入 Supabase URL/Key 才初始化\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e頁面載入自動初始化\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e儲存設定到 localStorage\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e不使用 localStorage\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003everifyAdmin()\u003c/code\u003e 前端比對 email\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e登入成功即顯示管理面板，權限交 RLS\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003econfig-section 手動設定區域\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e改為 status-section 自動狀態顯示\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/tbody\u003e\u003c/table\u003e\n\n操作功能：\n\n*   ✅ 新增公告（寫入 `created_at`、`updated_at`、`status='draft'`、`is_pinned=false`）\n    \n*   ✅ 編輯公告（寫入 `updated_at`）\n    \n*   ✅ 發布（寫入 `status='published'`、`published_at`、`updated_at`）\n    \n*   ✅ 下架（寫入 `status='draft'`、`published_at=null`、`updated_at`）\n    \n*   ✅ 置頂/取消置頂（寫入 `is_pinned`、`updated_at`）\n    \n*   ✅ 登出\n    \n*   ❌ 無刪除按鈕、無刪除函式、無 DELETE API 呼叫\n    \n*   ✅ Magic Link 使用 `emailRedirectTo: window.location.href.split('#')[0]`\n    \n\n* * *\n\n## C. Supabase RLS SQL 草案\n\n### 第 1 段：唯讀盤點（不修改任何設定）\n\n```sql\n-- 查 announcements 欄位\nSELECT column_name, data_type, is_nullable, column_default\nFROM information_schema.columns\nWHERE table_schema = 'public' AND table_name = 'announcements'\nORDER BY ordinal_position;\n\n-- 查 RLS 是否啟用\nSELECT tablename, rowsecurity FROM pg_tables\nWHERE schemaname = 'public' AND tablename = 'announcements';\n\n-- 查現有 policy\nSELECT schemaname, tablename, policyname, cmd, roles, qual, with_check\nFROM pg_policies\nWHERE schemaname = 'public' AND tablename = 'announcements';\n\n-- 查權限\nSELECT grantee, privilege_type FROM information_schema.table_privileges\nWHERE table_schema = 'public' AND table_name = 'announcements';\n```\n\n### 第 2 段：取得 ADMIN_AUTH_UID\n\n```sql\nSELECT id, email, created_at FROM auth.users\nWHERE email = 'mj29379898@gmail.com';\n```\n\n\u003e 取回傳的 `id` 作為 `ADMIN_AUTH_UID`，不可猜測或填假值。\n\n### 第 3 段：待確認後執行的 RLS 修復\n\n設計原則：\n\n*   公開訪客 → SELECT status='published'\n    \n*   管理員 UID → SELECT 全部 + INSERT + UPDATE\n    \n*   無 DELETE policy\n    \n*   使用 `auth.uid() = '\u003cADMIN_AUTH_UID\u003e'::uuid`（不用 `auth.role()`）\n    \n\n```sql\nALTER TABLE announcements ENABLE ROW LEVEL SECURITY;\n\n-- 防呆：撤銷 DELETE\nREVOKE DELETE ON announcements FROM anon, authenticated;\n\n-- 公開可讀已發布\nCREATE POLICY \"anon_select_published\" ON announcements\n  FOR SELECT TO anon, authenticated\n  USING (status = 'published');\n\n-- 管理員可讀全部\nCREATE POLICY \"admin_select_all\" ON announcements\n  FOR SELECT TO authenticated\n  USING (auth.uid() = '\u003cADMIN_AUTH_UID\u003e'::uuid);\n\n-- 管理員可新增\nCREATE POLICY \"admin_insert\" ON announcements\n  FOR INSERT TO authenticated\n  WITH CHECK (auth.uid() = '\u003cADMIN_AUTH_UID\u003e'::uuid);\n\n-- 管理員可更新\nCREATE POLICY \"admin_update\" ON announcements\n  FOR UPDATE TO authenticated\n  USING (auth.uid() = '\u003cADMIN_AUTH_UID\u003e'::uuid)\n  WITH CHECK (auth.uid() = '\u003cADMIN_AUTH_UID\u003e'::uuid);\n```\n\n影響：非管理員登入者 INSERT/UPDATE 被 RLS 拒絕。  \nRollback：`DROP POLICY IF EXISTS` 上述所有新建 policy。  \n不可執行條件：盤點未完成、UID 未取得、未獲授權。\n\n* * *\n\n## D. Magic Link Redirect URL\n\n**設定位置**：Supabase → Authentication → URL Configuration → Redirect URLs\n\n**須新增**：\n\n```\nhttps://muzha-shengmu.github.io/muzha-shengmu-test/announcement-admin.html\n```\n\n* * *\n\n## E. 測試步驟（15 項）\n\n1.  前台只看到 published\n    \n2.  前台無公告時顯示提示\n    \n3.  前台排序正確（置頂優先 + 時間倒序）\n    \n4.  未登入不可新增\n    \n5.  非管理員登入不可新增\n    \n6.  非管理員登入不可修改\n    \n7.  非管理員登入不可發布\n    \n8.  管理員可完整操作\n    \n9.  DELETE API 呼叫失敗\n    \n10.  後台無刪除按鈕\n    \n11.  發布後前台可讀取\n    \n12.  下架後前台不可讀取\n    \n13.  updated_at 每次操作更新\n    \n14.  前台不需 localStorage\n    \n15.  後台自動初始化\n    \n\n* * *\n\n## F. GitHub 推送前差異摘要\n\n```\n分支：cubelv-cli-announcement-hardening-v2（父：main, fe7118f）\n\n index.html                | +14 -9  改寫 script（硬編碼 + 自動初始化）\n announcement-admin.html   | +63 -72 重構（移除 manual init、email 驗證、刪除）\n──────────────────────────┼────────\n 2 files, +77 -81, net -4\n```\n\n未修改：CSS、HTML 結構、GitHub Pages 設定、任何 Supabase 正式資料。\n\n* * *\n\n\u003e **目前狀態：僅完成程式碼與 SQL 草案輸出，尚未修改 GitHub、Supabase RLS、資料表、Auth 設定、Redirect URL 或正式網站。**\n\n​","createdAt":1783228968452,"deletedAt":null,"id":"279b3842564091b6788d4524","isNew":false,"isPublic":true,"itemType":"NOTE","name":"修復計畫 v2：公告系統安全與可用性（A~F 完整報告）","parents":{"2a5bb84fc5a46bee9113f20d":1783228968452},"preParentID":null,"updatedAt":1783239685096,"version":6,"websiteContent":"[{\"originUrl\":\"https://YOUR-PROJECT.supabase.co\",\"title\":null,\"content\":null,\"imageUrl\":null},{\"originUrl\":\"https://muzha-shengmu.github.io/muzha-shengmu-test/announcement-admin.html\",\"title\":null,\"content\":null,\"imageUrl\":null}]"},"ownerName":"Sun Lin","subtree":[{"content":"\u003e **日期**：2026-07-05  \n\u003e **分支**：`cubelv-cli-announcement-hardening-v2`（尚未 push）  \n\u003e **狀態**：僅完成程式碼與 SQL 草案輸出，尚未修改任何正式系統。\n\n* * *\n\n## A. index.html 修改摘要\n\n\u003ctable style=\"min-width: 50px;\"\u003e\u003ccolgroup\u003e\u003ccol style=\"min-width: 25px;\"\u003e\u003ccol style=\"min-width: 25px;\"\u003e\u003c/colgroup\u003e\u003ctbody\u003e\u003ctr\u003e\u003cth colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e原來 ❌\u003c/p\u003e\u003c/th\u003e\u003cth colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e改後 ✅\u003c/p\u003e\u003c/th\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e從 \u003ccode\u003elocalStorage\u003c/code\u003e 讀 Supabase 設定\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e硬編碼 \u003ccode\u003eSUPABASE_URL\u003c/code\u003e、\u003ccode\u003eSUPABASE_PUBLISHABLE_KEY\u003c/code\u003e 常數\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e無 localStorage → 顯示錯誤\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e頁面載入自動初始化，直接讀公告\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003eloadPublishedAnnouncements(url, key)\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e無參數版本\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/tbody\u003e\u003c/table\u003e\n\n關鍵程式碼：\n\n```js\nconst SUPABASE_URL = 'https://YOUR-PROJECT.supabase.co';\nconst SUPABASE_PUBLISHABLE_KEY = 'sb_publishable_YOUR_KEY';\n\n(function init() {\n  if (!SUPABASE_URL.includes('YOUR-PROJECT') \u0026\u0026 !SUPABASE_PUBLISHABLE_KEY.includes('YOUR_KEY')) {\n    loadPublishedAnnouncements();\n  }\n})();\n```\n\n*   ✅ 不依賴 localStorage\n    \n*   ✅ 只讀取 `status = 'published'` 的公告\n    \n*   ✅ 排序：`is_pinned` DESC → `published_at` DESC\n    \n*   ✅ 無公告時顯示「目前尚無已發布公告。」\n    \n*   ✅ 絕不使用 service_role key / secret key\n    \n\n* * *\n\n## B. announcement-admin.html 修改摘要\n\n\u003ctable style=\"min-width: 50px;\"\u003e\u003ccolgroup\u003e\u003ccol style=\"min-width: 25px;\"\u003e\u003ccol style=\"min-width: 25px;\"\u003e\u003c/colgroup\u003e\u003ctbody\u003e\u003ctr\u003e\u003cth colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e原來 ❌\u003c/p\u003e\u003c/th\u003e\u003cth colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e改後 ✅\u003c/p\u003e\u003c/th\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e手動輸入 Supabase URL/Key 才初始化\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e頁面載入自動初始化\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e儲存設定到 localStorage\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e不使用 localStorage\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003everifyAdmin()\u003c/code\u003e 前端比對 email\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e登入成功即顯示管理面板，權限交 RLS\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003econfig-section 手動設定區域\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e改為 status-section 自動狀態顯示\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/tbody\u003e\u003c/table\u003e\n\n操作功能：\n\n*   ✅ 新增公告（寫入 `created_at`、`updated_at`、`status='draft'`、`is_pinned=false`）\n    \n*   ✅ 編輯公告（寫入 `updated_at`）\n    \n*   ✅ 發布（寫入 `status='published'`、`published_at`、`updated_at`）\n    \n*   ✅ 下架（寫入 `status='draft'`、`published_at=null`、`updated_at`）\n    \n*   ✅ 置頂/取消置頂（寫入 `is_pinned`、`updated_at`）\n    \n*   ✅ 登出\n    \n*   ❌ 無刪除按鈕、無刪除函式、無 DELETE API 呼叫\n    \n*   ✅ Magic Link 使用 `emailRedirectTo: window.location.href.split('#')[0]`\n    \n\n* * *\n\n## C. Supabase RLS SQL 草案\n\n### 第 1 段：唯讀盤點（不修改任何設定）\n\n```sql\n-- 查 announcements 欄位\nSELECT column_name, data_type, is_nullable, column_default\nFROM information_schema.columns\nWHERE table_schema = 'public' AND table_name = 'announcements'\nORDER BY ordinal_position;\n\n-- 查 RLS 是否啟用\nSELECT tablename, rowsecurity FROM pg_tables\nWHERE schemaname = 'public' AND tablename = 'announcements';\n\n-- 查現有 policy\nSELECT schemaname, tablename, policyname, cmd, roles, qual, with_check\nFROM pg_policies\nWHERE schemaname = 'public' AND tablename = 'announcements';\n\n-- 查權限\nSELECT grantee, privilege_type FROM information_schema.table_privileges\nWHERE table_schema = 'public' AND table_name = 'announcements';\n```\n\n### 第 2 段：取得 ADMIN_AUTH_UID\n\n```sql\nSELECT id, email, created_at FROM auth.users\nWHERE email = 'mj29379898@gmail.com';\n```\n\n\u003e 取回傳的 `id` 作為 `ADMIN_AUTH_UID`，不可猜測或填假值。\n\n### 第 3 段：待確認後執行的 RLS 修復\n\n設計原則：\n\n*   公開訪客 → SELECT status='published'\n    \n*   管理員 UID → SELECT 全部 + INSERT + UPDATE\n    \n*   無 DELETE policy\n    \n*   使用 `auth.uid() = '\u003cADMIN_AUTH_UID\u003e'::uuid`（不用 `auth.role()`）\n    \n\n```sql\nALTER TABLE announcements ENABLE ROW LEVEL SECURITY;\n\n-- 防呆：撤銷 DELETE\nREVOKE DELETE ON announcements FROM anon, authenticated;\n\n-- 公開可讀已發布\nCREATE POLICY \"anon_select_published\" ON announcements\n  FOR SELECT TO anon, authenticated\n  USING (status = 'published');\n\n-- 管理員可讀全部\nCREATE POLICY \"admin_select_all\" ON announcements\n  FOR SELECT TO authenticated\n  USING (auth.uid() = '\u003cADMIN_AUTH_UID\u003e'::uuid);\n\n-- 管理員可新增\nCREATE POLICY \"admin_insert\" ON announcements\n  FOR INSERT TO authenticated\n  WITH CHECK (auth.uid() = '\u003cADMIN_AUTH_UID\u003e'::uuid);\n\n-- 管理員可更新\nCREATE POLICY \"admin_update\" ON announcements\n  FOR UPDATE TO authenticated\n  USING (auth.uid() = '\u003cADMIN_AUTH_UID\u003e'::uuid)\n  WITH CHECK (auth.uid() = '\u003cADMIN_AUTH_UID\u003e'::uuid);\n```\n\n影響：非管理員登入者 INSERT/UPDATE 被 RLS 拒絕。  \nRollback：`DROP POLICY IF EXISTS` 上述所有新建 policy。  \n不可執行條件：盤點未完成、UID 未取得、未獲授權。\n\n* * *\n\n## D. Magic Link Redirect URL\n\n**設定位置**：Supabase → Authentication → URL Configuration → Redirect URLs\n\n**須新增**：\n\n```\nhttps://muzha-shengmu.github.io/muzha-shengmu-test/announcement-admin.html\n```\n\n* * *\n\n## E. 測試步驟（15 項）\n\n1.  前台只看到 published\n    \n2.  前台無公告時顯示提示\n    \n3.  前台排序正確（置頂優先 + 時間倒序）\n    \n4.  未登入不可新增\n    \n5.  非管理員登入不可新增\n    \n6.  非管理員登入不可修改\n    \n7.  非管理員登入不可發布\n    \n8.  管理員可完整操作\n    \n9.  DELETE API 呼叫失敗\n    \n10.  後台無刪除按鈕\n    \n11.  發布後前台可讀取\n    \n12.  下架後前台不可讀取\n    \n13.  updated_at 每次操作更新\n    \n14.  前台不需 localStorage\n    \n15.  後台自動初始化\n    \n\n* * *\n\n## F. GitHub 推送前差異摘要\n\n```\n分支：cubelv-cli-announcement-hardening-v2（父：main, fe7118f）\n\n index.html                | +14 -9  改寫 script（硬編碼 + 自動初始化）\n announcement-admin.html   | +63 -72 重構（移除 manual init、email 驗證、刪除）\n──────────────────────────┼────────\n 2 files, +77 -81, net -4\n```\n\n未修改：CSS、HTML 結構、GitHub Pages 設定、任何 Supabase 正式資料。\n\n* * *\n\n\u003e **目前狀態：僅完成程式碼與 SQL 草案輸出，尚未修改 GitHub、Supabase RLS、資料表、Auth 設定、Redirect URL 或正式網站。**\n\n​","createdAt":1783228968452,"deletedAt":null,"id":"279b3842564091b6788d4524","isNew":false,"isPublic":true,"itemType":"NOTE","name":"修復計畫 v2：公告系統安全與可用性（A~F 完整報告）","parents":{"2a5bb84fc5a46bee9113f20d":1783228968452},"preParentID":null,"updatedAt":1783239685096,"version":6,"websiteContent":"[{\"originUrl\":\"https://YOUR-PROJECT.supabase.co\",\"title\":null,\"content\":null,\"imageUrl\":null},{\"originUrl\":\"https://muzha-shengmu.github.io/muzha-shengmu-test/announcement-admin.html\",\"title\":null,\"content\":null,\"imageUrl\":null}]"}]}