fix(web+k8s): CSRF mismatch + NetworkPolicy 缺少監控端口
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 12m19s
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 12m19s
1. pending-approvals-card: 改為點擊時即時 fetch 新 CSRF token 避免多 useCSRF 實例互相覆蓋 cookie 導致 header/cookie 不一致 2. NetworkPolicy: 補開 110:3002(Grafana) 9090(Prometheus) 3001(Gitea) 修正 monitoring probe "All connection attempts failed" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,11 +10,21 @@ 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'
|
||||
import { CURRENT_USER } from '@/lib/constants/user'
|
||||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ''
|
||||
|
||||
// 2026-04-09 Claude Sonnet 4.6: 每次操作前直接 fetch 新 CSRF token
|
||||
// 避免多個 useCSRF 實例互相覆蓋 cookie,導致 header token 和 cookie 不一致
|
||||
async function fetchFreshCsrfHeaders(): Promise<Record<string, string>> {
|
||||
try {
|
||||
const r = await fetch(`${API_BASE}/api/v1/csrf/token`, { method: 'GET', credentials: 'include' })
|
||||
if (!r.ok) return {}
|
||||
const d = await r.json()
|
||||
return d.token ? { 'X-CSRF-Token': d.token } : {}
|
||||
} catch { return {} }
|
||||
}
|
||||
|
||||
interface Approval {
|
||||
id: string
|
||||
action: string
|
||||
@@ -35,7 +45,6 @@ export function PendingApprovalsCard() {
|
||||
const t = useTranslations('dashboard')
|
||||
const router = useRouter()
|
||||
const locale = useLocale()
|
||||
const { getHeaders } = useCSRF()
|
||||
const [approvals, setApprovals] = useState<Approval[]>([])
|
||||
const [actionError, setActionError] = useState<string | null>(null)
|
||||
const [actioningId, setActioningId] = useState<string | null>(null)
|
||||
@@ -83,27 +92,33 @@ export function PendingApprovalsCard() {
|
||||
<div style={{ display: 'flex', gap: 4, marginTop: 5 }}>
|
||||
<button
|
||||
disabled={actioningId === ap.id}
|
||||
onClick={() => {
|
||||
onClick={async () => {
|
||||
setActionError(null)
|
||||
setActioningId(ap.id)
|
||||
fetch(`${API_BASE}/api/v1/approvals/${ap.id}/sign`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', ...getHeaders() }, body: JSON.stringify({ signer_id: CURRENT_USER.id, signer_name: CURRENT_USER.name }) })
|
||||
.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}`))
|
||||
.finally(() => setActioningId(null))
|
||||
try {
|
||||
const csrfHeaders = await fetchFreshCsrfHeaders()
|
||||
const r = await fetch(`${API_BASE}/api/v1/approvals/${ap.id}/sign`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', ...csrfHeaders }, body: JSON.stringify({ signer_id: CURRENT_USER.id, signer_name: CURRENT_USER.name }) })
|
||||
if (!r.ok) throw new Error(`${r.status}`)
|
||||
setApprovals(prev => prev.filter(x => x.id !== ap.id))
|
||||
} catch (e: unknown) {
|
||||
setActionError(`approve failed: ${e instanceof Error ? e.message : String(e)}`)
|
||||
} finally { setActioningId(null) }
|
||||
}}
|
||||
style={{ flex: 1, padding: 6, border: 'none', borderRadius: 5, fontSize: 11, fontWeight: 600, cursor: actioningId === ap.id ? 'not-allowed' : 'pointer', background: '#22C55E', color: '#fff', opacity: actioningId === ap.id ? 0.6 : 1 }}
|
||||
>{actioningId === ap.id ? '...' : t('approve')}</button>
|
||||
<button
|
||||
disabled={actioningId === ap.id}
|
||||
onClick={() => {
|
||||
onClick={async () => {
|
||||
setActionError(null)
|
||||
setActioningId(ap.id)
|
||||
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}`))
|
||||
.finally(() => setActioningId(null))
|
||||
try {
|
||||
const csrfHeaders = await fetchFreshCsrfHeaders()
|
||||
const r = await fetch(`${API_BASE}/api/v1/approvals/${ap.id}/reject`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', ...csrfHeaders }, body: JSON.stringify({ reason: 'rejected-from-web' }) })
|
||||
if (!r.ok) throw new Error(`${r.status}`)
|
||||
setApprovals(prev => prev.filter(x => x.id !== ap.id))
|
||||
} catch (e: unknown) {
|
||||
setActionError(`reject failed: ${e instanceof Error ? e.message : String(e)}`)
|
||||
} finally { setActioningId(null) }
|
||||
}}
|
||||
style={{ flex: 1, padding: 6, border: '0.5px solid #e0ddd4', borderRadius: 5, fontSize: 11, cursor: actioningId === ap.id ? 'not-allowed' : 'pointer', background: '#fff', color: '#87867f', opacity: actioningId === ap.id ? 0.6 : 1 }}
|
||||
>{t('reject')}</button>
|
||||
|
||||
@@ -125,10 +125,11 @@ spec:
|
||||
- protocol: TCP
|
||||
port: 11434
|
||||
|
||||
# 允許訪問 192.168.0.110 DevOps 金庫 (Harbor + Sentry + Langfuse + SSH Repair)
|
||||
# 允許訪問 192.168.0.110 DevOps 金庫 (Harbor + Sentry + Langfuse + SSH Repair + Monitoring)
|
||||
# 2026-03-24 新增: Sentry Self-Hosted
|
||||
# 2026-03-26 新增: Langfuse LLMOps (Phase 15.1)
|
||||
# 2026-04-09 新增: SSH port 22 — repair-bot-110.sh 自動修復 (Bug #11 修正)
|
||||
# 2026-04-09 新增: Grafana(3002) + Prometheus(9090) + Gitea(3001) — monitoring probe
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: 192.168.0.110/32
|
||||
@@ -146,6 +147,15 @@ spec:
|
||||
# ADR-062: SSH_COMMAND Playbook 需要 K8s Pod → 110:22 的 egress
|
||||
- protocol: TCP
|
||||
port: 22
|
||||
# Grafana — monitoring probe + 監控工具卡片
|
||||
- protocol: TCP
|
||||
port: 3002
|
||||
# Prometheus — monitoring probe + 監控工具卡片
|
||||
- protocol: TCP
|
||||
port: 9090
|
||||
# Gitea — CI/CD 主倉 probe + monitoring
|
||||
- protocol: TCP
|
||||
port: 3001
|
||||
|
||||
# 允許訪問 192.168.0.112 安全掃描服務
|
||||
- to:
|
||||
|
||||
Reference in New Issue
Block a user