mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-02 15:23:19 +08:00
fix: CSRF token accessibility and hook installation status
- Remove HttpOnly from XSRF-TOKEN cookie for JavaScript readability - Add hook installation status detection in system settings API - Update InjectionControlTab to show installed hooks status - Add brace expansion support in globToRegex utility
This commit is contained in:
@@ -46,7 +46,7 @@ import {
|
||||
Layers,
|
||||
Filter,
|
||||
} from 'lucide-react';
|
||||
import { useInstallRecommendedHooks } from '@/hooks/useSystemSettings';
|
||||
import { useInstallRecommendedHooks, useSystemSettings } from '@/hooks/useSystemSettings';
|
||||
import type { InjectionPreviewFile, InjectionPreviewResponse } from '@/lib/api';
|
||||
import { getInjectionPreview, COMMAND_PREVIEWS, type CommandPreviewConfig } from '@/lib/api';
|
||||
|
||||
@@ -197,6 +197,9 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
||||
// State for hooks installation
|
||||
const [installingHookIds, setInstallingHookIds] = useState<string[]>([]);
|
||||
|
||||
// Fetch system settings (for hooks installation status)
|
||||
const systemSettingsQuery = useSystemSettings();
|
||||
|
||||
// State for injection preview
|
||||
const [previewMode, setPreviewMode] = useState<'required' | 'all'>('required');
|
||||
const [categoryFilter, setCategoryFilter] = useState<SpecCategory | 'all'>('all');
|
||||
@@ -349,10 +352,18 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
||||
|
||||
const installedHookIds = useMemo(() => {
|
||||
const installed = new Set<string>();
|
||||
const hooks = systemSettingsQuery.data?.recommendedHooks;
|
||||
if (hooks) {
|
||||
hooks.forEach(hook => {
|
||||
if (hook.installed) {
|
||||
installed.add(hook.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
return installed;
|
||||
}, []);
|
||||
}, [systemSettingsQuery.data?.recommendedHooks]);
|
||||
|
||||
const installedCount = 0;
|
||||
const installedCount = installedHookIds.size;
|
||||
const allHooksInstalled = installedCount === RECOMMENDED_HOOKS.length;
|
||||
|
||||
const handleInstallHook = useCallback(async (hookId: string) => {
|
||||
|
||||
@@ -7555,6 +7555,7 @@ export interface SystemSettings {
|
||||
description: string;
|
||||
scope: 'global' | 'project';
|
||||
autoInstall: boolean;
|
||||
installed: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"aria": {
|
||||
"toggleNavigation": "Toggle navigation menu",
|
||||
"refreshWorkspace": "Refresh workspace",
|
||||
|
||||
@@ -303,13 +303,16 @@
|
||||
"hookCommand": "Command",
|
||||
"hookScope": "Scope",
|
||||
"hookTimeout": "Timeout (ms)",
|
||||
"hookFailMode": "Fail Mode"
|
||||
"hookFailMode": "Fail Mode",
|
||||
"editTitle": "Edit Spec: {title}",
|
||||
"editDescription": "Modify spec metadata and settings."
|
||||
},
|
||||
|
||||
"form": {
|
||||
"readMode": "Read Mode",
|
||||
"priority": "Priority",
|
||||
"keywords": "Keywords",
|
||||
"keywordsPlaceholder": "Enter keywords, press Enter or comma to add",
|
||||
"title": "Title",
|
||||
"titlePlaceholder": "Enter spec title",
|
||||
"addKeyword": "Add Keyword",
|
||||
@@ -322,11 +325,6 @@
|
||||
"titleRequired": "Title is required"
|
||||
},
|
||||
|
||||
"dialog": {
|
||||
"editTitle": "Edit Spec: {title}",
|
||||
"editDescription": "Modify spec metadata and settings."
|
||||
},
|
||||
|
||||
"hooks": {
|
||||
"installSuccess": "Hook installed successfully",
|
||||
"installError": "Failed to install hook",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"cancel": "取消",
|
||||
"save": "保存",
|
||||
"aria": {
|
||||
"toggleNavigation": "切换导航菜单",
|
||||
"refreshWorkspace": "刷新工作区",
|
||||
|
||||
@@ -310,13 +310,16 @@
|
||||
"hookCommand": "执行命令",
|
||||
"hookScope": "作用域",
|
||||
"hookTimeout": "超时时间(ms)",
|
||||
"hookFailMode": "失败模式"
|
||||
"hookFailMode": "失败模式",
|
||||
"editTitle": "编辑规范:{title}",
|
||||
"editDescription": "修改规范元数据和设置。"
|
||||
},
|
||||
|
||||
"form": {
|
||||
"readMode": "读取模式",
|
||||
"priority": "优先级",
|
||||
"keywords": "关键词",
|
||||
"keywordsPlaceholder": "输入关键词,按回车或逗号添加",
|
||||
"title": "标题",
|
||||
"titlePlaceholder": "输入规范标题",
|
||||
"addKeyword": "添加关键词",
|
||||
@@ -329,11 +332,6 @@
|
||||
"titleRequired": "标题为必填项"
|
||||
},
|
||||
|
||||
"dialog": {
|
||||
"editTitle": "编辑规范:{title}",
|
||||
"editDescription": "修改规范元数据和设置。"
|
||||
},
|
||||
|
||||
"hooks": {
|
||||
"installSuccess": "钩子安装成功",
|
||||
"installError": "钩子安装失败",
|
||||
|
||||
@@ -51,7 +51,8 @@ function setCsrfCookie(res: ServerResponse, token: string, maxAgeSeconds: number
|
||||
const attributes = [
|
||||
`XSRF-TOKEN=${encodeURIComponent(token)}`,
|
||||
'Path=/',
|
||||
'HttpOnly',
|
||||
// Note: XSRF-TOKEN must be readable by JavaScript for CSRF protection to work
|
||||
// The token is also sent via X-CSRF-Token header, so not having HttpOnly is safe
|
||||
'SameSite=Strict',
|
||||
`Max-Age=${maxAgeSeconds}`,
|
||||
];
|
||||
|
||||
@@ -71,7 +71,8 @@ function setCsrfCookie(res: ServerResponse, token: string, maxAgeSeconds: number
|
||||
const attributes = [
|
||||
`XSRF-TOKEN=${encodeURIComponent(token)}`,
|
||||
'Path=/',
|
||||
'HttpOnly',
|
||||
// Note: XSRF-TOKEN must be readable by JavaScript for CSRF protection to work
|
||||
// The token is also sent via X-CSRF-Token header, so not having HttpOnly is safe
|
||||
'SameSite=Strict',
|
||||
`Max-Age=${maxAgeSeconds}`,
|
||||
];
|
||||
|
||||
@@ -90,6 +90,27 @@ function readSettingsFile(filePath: string): Record<string, unknown> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a recommended hook is installed in settings
|
||||
*/
|
||||
function isHookInstalled(
|
||||
settings: Record<string, unknown> & { hooks?: Record<string, unknown[]> },
|
||||
hook: typeof RECOMMENDED_HOOKS[number]
|
||||
): boolean {
|
||||
const hooks = settings.hooks;
|
||||
if (!hooks) return false;
|
||||
|
||||
const eventHooks = hooks[hook.event];
|
||||
if (!eventHooks || !Array.isArray(eventHooks)) return false;
|
||||
|
||||
// Check if hook exists in nested hooks array (by command)
|
||||
return eventHooks.some((entry) => {
|
||||
const entryHooks = (entry as Record<string, unknown>).hooks as Array<Record<string, unknown>> | undefined;
|
||||
if (!entryHooks || !Array.isArray(entryHooks)) return false;
|
||||
return entryHooks.some((h) => (h as Record<string, unknown>).command === hook.command);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get system settings from global settings file
|
||||
*/
|
||||
@@ -97,12 +118,18 @@ function getSystemSettings(): {
|
||||
injectionControl: typeof DEFAULT_INJECTION_CONTROL;
|
||||
personalSpecDefaults: typeof DEFAULT_PERSONAL_SPEC_DEFAULTS;
|
||||
devProgressInjection: typeof DEFAULT_DEV_PROGRESS_INJECTION;
|
||||
recommendedHooks: typeof RECOMMENDED_HOOKS;
|
||||
recommendedHooks: Array<typeof RECOMMENDED_HOOKS[number] & { installed: boolean }>;
|
||||
} {
|
||||
const settings = readSettingsFile(GLOBAL_SETTINGS_PATH) as Record<string, unknown>;
|
||||
const settings = readSettingsFile(GLOBAL_SETTINGS_PATH) as Record<string, unknown> & { hooks?: Record<string, unknown[]> };
|
||||
const system = (settings.system || {}) as Record<string, unknown>;
|
||||
const user = (settings.user || {}) as Record<string, unknown>;
|
||||
|
||||
// Check installation status for each recommended hook
|
||||
const recommendedHooksWithStatus = RECOMMENDED_HOOKS.map(hook => ({
|
||||
...hook,
|
||||
installed: isHookInstalled(settings, hook)
|
||||
}));
|
||||
|
||||
return {
|
||||
injectionControl: {
|
||||
...DEFAULT_INJECTION_CONTROL,
|
||||
@@ -116,7 +143,7 @@ function getSystemSettings(): {
|
||||
...DEFAULT_DEV_PROGRESS_INJECTION,
|
||||
...((system.devProgressInjection || {}) as Record<string, unknown>)
|
||||
} as typeof DEFAULT_DEV_PROGRESS_INJECTION,
|
||||
recommendedHooks: RECOMMENDED_HOOKS
|
||||
recommendedHooks: recommendedHooksWithStatus
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -257,7 +257,8 @@ function setCsrfCookie(res: http.ServerResponse, token: string, maxAgeSeconds: n
|
||||
const attributes = [
|
||||
`XSRF-TOKEN=${encodeURIComponent(token)}`,
|
||||
'Path=/',
|
||||
'HttpOnly',
|
||||
// Note: XSRF-TOKEN must be readable by JavaScript for CSRF protection to work
|
||||
// The token is also sent via X-CSRF-Token header, so not having HttpOnly is safe
|
||||
'SameSite=Strict',
|
||||
`Max-Age=${maxAgeSeconds}`,
|
||||
];
|
||||
|
||||
@@ -64,8 +64,25 @@ export function isBinaryFile(filePath: string): boolean {
|
||||
|
||||
/**
|
||||
* Convert glob pattern to regex
|
||||
* Supports: *, ?, and brace expansion {a,b,c}
|
||||
*/
|
||||
export function globToRegex(pattern: string): RegExp {
|
||||
// Handle brace expansion: *.{md,json,ts} -> (?:.*\.md|.*\.json|.*\.ts)
|
||||
const braceMatch = pattern.match(/^(.*)\{([^}]+)\}(.*)$/);
|
||||
if (braceMatch) {
|
||||
const [, prefix, options, suffix] = braceMatch;
|
||||
const optionList = options.split(',').map(opt => `${prefix}${opt}${suffix}`);
|
||||
// Create a regex that matches any of the expanded patterns
|
||||
const expandedPatterns = optionList.map(opt => {
|
||||
return opt
|
||||
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
||||
.replace(/\*/g, '.*')
|
||||
.replace(/\?/g, '.');
|
||||
});
|
||||
return new RegExp(`^(?:${expandedPatterns.join('|')})$`, 'i');
|
||||
}
|
||||
|
||||
// Standard glob conversion
|
||||
const escaped = pattern
|
||||
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
||||
.replace(/\*/g, '.*')
|
||||
|
||||
Reference in New Issue
Block a user