Files
awoooi/apps/web/src/hooks/useCSRF.ts
OG T 26839227ff fix(web): 修復 TypeScript 錯誤
- useCSRF: 修正 import 路徑 @/lib/env → @/lib/config
- terminal-telemetry: 新增 UNKNOWN_COMPONENT 錯誤碼

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-28 19:06:44 +08:00

121 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* CSRF Token Hook (Phase 20)
* ==========================
* Phase 19 首席架構師審查: 核鑰 UX 缺 CSRF 防護 (9/10)
*
* 使用方式:
* 1. 在需要 CSRF 保護的組件中使用 useCSRF()
* 2. 在敏感請求中帶上 X-CSRF-Token header
*
* @example
* ```tsx
* const { csrfToken, isLoading, getHeaders } = useCSRF();
*
* const handleApprove = async () => {
* await fetch('/api/v1/approvals/123/sign', {
* method: 'POST',
* headers: {
* 'Content-Type': 'application/json',
* ...getHeaders(), // 自動帶上 X-CSRF-Token
* },
* body: JSON.stringify({ ... }),
* });
* };
* ```
*
* 建立日期: 2026-03-28 (台北時間)
* 建立者: Claude Code (首席架構師)
*/
import { useCallback, useEffect, useState } from "react";
import { getApiUrl } from "@/lib/config";
interface CSRFTokenResponse {
token: string;
cookie_name: string;
header_name: string;
}
interface UseCSRFReturn {
/** CSRF Token (null 表示尚未載入) */
csrfToken: string | null;
/** 是否正在載入 */
isLoading: boolean;
/** 載入錯誤 */
error: Error | null;
/** 取得包含 CSRF header 的物件,可直接展開到 fetch headers */
getHeaders: () => Record<string, string>;
/** 手動重新獲取 Token */
refresh: () => Promise<void>;
}
/**
* CSRF Token Hook
*
* 自動在 mount 時獲取 CSRF Token並提供 getHeaders() 方法
* 供敏感請求使用。
*/
export function useCSRF(): UseCSRFReturn {
const [csrfToken, setCSRFToken] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const fetchToken = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const apiUrl = getApiUrl();
const response = await fetch(`${apiUrl}/api/v1/csrf/token`, {
method: "GET",
credentials: "include", // 確保 cookie 被設定
});
if (!response.ok) {
throw new Error(`Failed to fetch CSRF token: ${response.status}`);
}
const data: CSRFTokenResponse = await response.json();
setCSRFToken(data.token);
} catch (err) {
setError(err instanceof Error ? err : new Error(String(err)));
console.error("[useCSRF] Failed to fetch CSRF token:", err);
} finally {
setIsLoading(false);
}
}, []);
// 自動在 mount 時獲取 Token
useEffect(() => {
fetchToken();
}, [fetchToken]);
// 提供 headers 物件,可直接展開到 fetch
const getHeaders = useCallback((): Record<string, string> => {
if (!csrfToken) {
console.warn("[useCSRF] CSRF token not available yet");
return {};
}
return {
"X-CSRF-Token": csrfToken,
};
}, [csrfToken]);
return {
csrfToken,
isLoading,
error,
getHeaders,
refresh: fetchToken,
};
}
/**
* CSRF Context Provider (可選,用於全局共享 Token)
*
* 如果多個組件需要共享同一個 CSRF Token
* 可以使用 CSRFProvider 包裝應用根組件。
*/
export { useCSRF as default };