fix(web): 活躍事件 Y/n 按鈕補上 CSRF Token (P0 根本原因)
All checks were successful
E2E Health Check / e2e-health (push) Successful in 19s
All checks were successful
E2E Health Check / e2e-health (push) Successful in 19s
問題: DualStateIncidentCard 的 Y/n 按鈕呼叫 apiClient.signApproval/rejectApproval 時,沒有帶 X-CSRF-Token header 也沒有 credentials: 'include' 後端返回 403 CSRF token cookie missing 修復: - api-client.ts: signApproval/rejectApproval 加入 csrfToken 參數 + X-CSRF-Token header + credentials: 'include' - dual-state-incident-card.tsx: 加入 useCSRF() hook, 將 csrfToken 傳入 API 呼叫,更新 useCallback deps Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,7 @@ import React, { useState, useCallback, useEffect, useRef } from 'react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import type { DecisionInfo } from '@/lib/api-client';
|
||||
import { apiClient } from '@/lib/api-client'
|
||||
import { useCSRF } from '@/hooks/useCSRF'
|
||||
|
||||
type ButtonState = 'idle' | 'loading' | 'approved' | 'rejected' | 'error' | 'timeout'
|
||||
|
||||
@@ -63,6 +64,7 @@ export const DualStateIncidentCard: React.FC<DualStateIncidentCardProps> = ({
|
||||
onApprovalChange,
|
||||
}) => {
|
||||
const t = useTranslations('incident.card')
|
||||
const { csrfToken } = useCSRF()
|
||||
const isAlert = status === 'alert'
|
||||
const [buttonState, setButtonState] = useState<ButtonState>('idle')
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
||||
@@ -132,8 +134,8 @@ export const DualStateIncidentCard: React.FC<DualStateIncidentCardProps> = ({
|
||||
throw new Error('No approval ID available')
|
||||
}
|
||||
|
||||
// Step 2: 簽核
|
||||
const result = await apiClient.signApproval(approvalId, 'commander', 'Authorized via WarRoom')
|
||||
// Step 2: 簽核 (Phase 22 P0: 帶入 CSRF token)
|
||||
const result = await apiClient.signApproval(approvalId, 'commander', 'Authorized via WarRoom', csrfToken)
|
||||
console.log('✅ 簽核回應:', result)
|
||||
|
||||
// 清除超時計時器
|
||||
@@ -170,7 +172,7 @@ export const DualStateIncidentCard: React.FC<DualStateIncidentCardProps> = ({
|
||||
// 不自動恢復,讓用戶看到錯誤並主動點擊重試
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- t is stable from next-intl
|
||||
}, [currentProposalId, decision, id, isDecisionReady, buttonState, onApprovalChange])
|
||||
}, [currentProposalId, decision, id, isDecisionReady, buttonState, onApprovalChange, csrfToken])
|
||||
|
||||
/**
|
||||
* 處理拒絕 (n 按鈕)
|
||||
@@ -210,7 +212,7 @@ export const DualStateIncidentCard: React.FC<DualStateIncidentCardProps> = ({
|
||||
throw new Error('No approval ID available')
|
||||
}
|
||||
|
||||
await apiClient.rejectApproval(approvalId, 'Rejected via WarRoom')
|
||||
await apiClient.rejectApproval(approvalId, 'Rejected via WarRoom', csrfToken)
|
||||
|
||||
// 清除超時計時器
|
||||
if (timeoutRef.current) {
|
||||
@@ -234,7 +236,7 @@ export const DualStateIncidentCard: React.FC<DualStateIncidentCardProps> = ({
|
||||
setErrorMessage(errMsg)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- t is stable from next-intl
|
||||
}, [currentProposalId, decision, id, isDecisionReady, buttonState, onApprovalChange])
|
||||
}, [currentProposalId, decision, id, isDecisionReady, buttonState, onApprovalChange, csrfToken])
|
||||
|
||||
/**
|
||||
* 渲染決策按鈕區塊
|
||||
|
||||
@@ -114,10 +114,14 @@ export const apiClient = {
|
||||
}>(res)
|
||||
},
|
||||
|
||||
async signApproval(approvalId: string, signer: string = 'commander', comment?: string) {
|
||||
async signApproval(approvalId: string, signer: string = 'commander', comment?: string, csrfToken?: string | null) {
|
||||
// Phase 22 P0: 加入 CSRF token + credentials (2026-03-31 Claude Code)
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
||||
if (csrfToken) headers['X-CSRF-Token'] = csrfToken
|
||||
const res = await fetch(`${API_BASE_URL}/approvals/${approvalId}/sign`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers,
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
signer_id: signer,
|
||||
signer_name: signer,
|
||||
@@ -138,10 +142,14 @@ export const apiClient = {
|
||||
}>(res)
|
||||
},
|
||||
|
||||
async rejectApproval(approvalId: string, reason?: string) {
|
||||
async rejectApproval(approvalId: string, reason?: string, csrfToken?: string | null) {
|
||||
// Phase 22 P0: 加入 CSRF token + credentials (2026-03-31 Claude Code)
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
||||
if (csrfToken) headers['X-CSRF-Token'] = csrfToken
|
||||
const res = await fetch(`${API_BASE_URL}/approvals/${approvalId}/reject`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers,
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
rejector_id: 'commander',
|
||||
rejector_name: 'Commander',
|
||||
|
||||
Reference in New Issue
Block a user