From 2b563b0fa87b46fea972875f376fab159212ea07 Mon Sep 17 00:00:00 2001 From: uncle6me-web Date: Tue, 14 Jul 2026 15:32:42 +0800 Subject: [PATCH] =?UTF-8?q?portal-auth=20P4=EF=BC=9A=E6=9C=80=E5=BE=8C?= =?UTF-8?q?=E4=B8=80=E5=80=8B=20active=20admin=20=E9=8E=96=E6=AD=BB?= =?UTF-8?q?=E4=BF=9D=E8=AD=B7=EF=BC=88=E5=81=9C=E7=94=A8/=E9=99=8D?= =?UTF-8?q?=E7=B4=9A=20=E2=86=92=20409=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 系統至少保留一個 active admin——bootstrap 只能跑一次(已有 admin 409), 若最後一個 admin 被停用/降級,帳號管理即永久鎖死。只在「目標是 active admin 且 patch 會使其失格」時多打一次 list(平時零成本)。 Co-Authored-By: Claude Fable 5 --- cypher-executor/src/routes/portal.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/cypher-executor/src/routes/portal.ts b/cypher-executor/src/routes/portal.ts index 93f935e..e485bb4 100644 --- a/cypher-executor/src/routes/portal.ts +++ b/cypher-executor/src/routes/portal.ts @@ -631,6 +631,21 @@ portalRouter.patch('/portal/admin/users/:id', (c) => patch.libraries = JSON.stringify(body.libraries); } if (Object.keys(patch).length === 0) return c.json({ error: '沒有可更新的欄位(status/role/libraries)' }, 400); + + // 鎖死保護(P4,總管派工明定):**不可停用/降級最後一個 active admin**—— + // 否則系統再無人能管帳號(bootstrap 只能跑一次,409),變成鎖死狀態。 + // 只在「目標現在是 active admin 且 patch 會使它不再是」時才多打一次 list(平時零成本)。 + const isActiveAdmin = (rec.values.role ?? '') === 'admin' && (rec.values.status ?? '') === 'active'; + const wouldLoseAdmin = patch.status === 'disabled' || patch.role === 'user'; + if (isActiveAdmin && wouldLoseAdmin) { + const all = await listRecordsByTemplate(c.env, USER_TEMPLATE); + const otherActiveAdmins = all.filter( + (u) => u.record_id !== recordId && (u.values.role ?? '') === 'admin' && (u.values.status ?? '') === 'active', + ); + if (otherActiveAdmins.length === 0) { + return c.json({ error: '不可停用或降級最後一個管理員——系統至少要保留一個 active admin' }, 409); + } + } patch.updated_at = new Date().toISOString(); const updated = await patchRecordValues(c.env, recordId, patch);