From 9a8f410f235bdadb96fb05a11a71bedb6ed50f12 Mon Sep 17 00:00:00 2001 From: OG T Date: Thu, 9 Apr 2026 19:00:02 +0800 Subject: [PATCH] =?UTF-8?q?fix(web):=20PendingApprovalsCard=20=E6=89=B9?= =?UTF-8?q?=E5=87=86/=E6=8B=92=E7=B5=95=E8=A3=9C=20CSRF=20Token=20?= =?UTF-8?q?=E2=80=94=20=E4=BF=AE=E5=BE=A9=20403?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因: fetch 沒帶 X-CSRF-Token header + credentials:include API 回 403 "CSRF token cookie missing" 修復: 加 useCSRF hook,sign/reject 請求帶 ...getHeaders() + credentials:include 與 incident-card.tsx / openclaw-state-machine.tsx 同一模式 Co-Authored-By: Claude Sonnet 4.6 --- apps/web/src/components/shared/pending-approvals-card.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/shared/pending-approvals-card.tsx b/apps/web/src/components/shared/pending-approvals-card.tsx index bcc55cf76..09b2cb383 100644 --- a/apps/web/src/components/shared/pending-approvals-card.tsx +++ b/apps/web/src/components/shared/pending-approvals-card.tsx @@ -10,6 +10,7 @@ import { useState, useEffect } from 'react' import { useTranslations } from 'next-intl' import { useRouter } from 'next/navigation' import { useLocale } from 'next-intl' +import { useCSRF } from '@/hooks/useCSRF' const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? '' @@ -33,6 +34,7 @@ export function PendingApprovalsCard() { const t = useTranslations('dashboard') const router = useRouter() const locale = useLocale() + const { getHeaders } = useCSRF() const [approvals, setApprovals] = useState([]) const [actionError, setActionError] = useState(null) const [actioningId, setActioningId] = useState(null) @@ -83,7 +85,7 @@ export function PendingApprovalsCard() { onClick={() => { setActionError(null) setActioningId(ap.id) - fetch(`${API_BASE}/api/v1/approvals/${ap.id}/sign`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ signer: 'web-ui' }) }) + fetch(`${API_BASE}/api/v1/approvals/${ap.id}/sign`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', ...getHeaders() }, body: JSON.stringify({ signer: 'web-ui' }) }) .then(r => { if (!r.ok) throw new Error(`${r.status}`); return r }) .then(() => setApprovals(prev => prev.filter(x => x.id !== ap.id))) .catch(e => setActionError(`approve failed: ${e.message}`)) @@ -96,7 +98,7 @@ export function PendingApprovalsCard() { onClick={() => { setActionError(null) setActioningId(ap.id) - fetch(`${API_BASE}/api/v1/approvals/${ap.id}/reject`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ reason: 'rejected-from-web' }) }) + fetch(`${API_BASE}/api/v1/approvals/${ap.id}/reject`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', ...getHeaders() }, body: JSON.stringify({ reason: 'rejected-from-web' }) }) .then(r => { if (!r.ok) throw new Error(`${r.status}`); return r }) .then(() => setApprovals(prev => prev.filter(x => x.id !== ap.id))) .catch(e => setActionError(`reject failed: ${e.message}`))