add external cors control panel

This commit is contained in:
Ahao
2025-06-27 13:29:38 +08:00
Unverified
parent cf437e6d94
commit c8a314fb41
6 changed files with 223 additions and 2 deletions

View File

@@ -0,0 +1,182 @@
import { BaseDialog } from "@/components/base";
import { useClash } from "@/hooks/use-clash";
import { showNotice } from "@/services/noticeService";
import {
Button,
Divider,
FormControlLabel,
List,
ListItem,
Switch,
TextField
} from "@mui/material";
import { Delete as DeleteIcon } from "@mui/icons-material";
import { useLockFn, useRequest } from "ahooks";
import { forwardRef, useImperativeHandle, useState } from "react";
import { useTranslation } from "react-i18next";
interface ClashHeaderConfigingRef {
open: () => void;
close: () => void;
}
export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
(props, ref) => {
const { t } = useTranslation();
const { clash, mutateClash, patchClash } = useClash();
const [open, setOpen] = useState(false);
// CORS配置状态管理
const [corsConfig, setCorsConfig] = useState<{
allowPrivateNetwork: boolean;
allowOrigins: string[];
}>(() => {
const cors = clash?.["external-controller-cors"];
return {
allowPrivateNetwork: cors?.["allow-private-network"] ?? true,
allowOrigins: cors?.["allow-origins"] ?? ["*"]
};
});
// 处理CORS配置变更
const handleCorsConfigChange = (
key: "allowPrivateNetwork" | "allowOrigins",
value: boolean | string[]
) => {
setCorsConfig(prev => ({
...prev,
[key]: value
}));
};
// 添加新的允许来源
const handleAddOrigin = () => {
handleCorsConfigChange("allowOrigins", [...corsConfig.allowOrigins, ""]);
};
// 更新允许来源列表中的某一项
const handleUpdateOrigin = (index: number, value: string) => {
const newOrigins = [...corsConfig.allowOrigins];
newOrigins[index] = value;
handleCorsConfigChange("allowOrigins", newOrigins);
};
// 删除允许来源列表中的某一项
const handleDeleteOrigin = (index: number) => {
const newOrigins = [...corsConfig.allowOrigins];
newOrigins.splice(index, 1);
handleCorsConfigChange("allowOrigins", newOrigins);
};
// 保存配置请求
const { loading, run: saveConfig } = useRequest(
async () => {
await patchClash({
"external-controller-cors": {
"allow-private-network": corsConfig.allowPrivateNetwork,
"allow-origins": corsConfig.allowOrigins.filter(origin => origin.trim() !== "")
}
});
await mutateClash();
},
{
manual: true,
onSuccess: () => {
setOpen(false);
showNotice("success", t("Configuration saved successfully"));
},
onError: () => {
showNotice("error", t("Failed to save configuration"));
}
}
);
useImperativeHandle(ref, () => ({
open: () => {
const cors = clash?.["external-controller-cors"];
setCorsConfig({
allowPrivateNetwork: cors?.["allow-private-network"] ?? true,
allowOrigins: cors?.["allow-origins"] ?? ["*"]
});
setOpen(true);
},
close: () => setOpen(false),
}));
const handleSave = useLockFn(async () => {
await saveConfig();
});
return (
<BaseDialog
open={open}
title={t("External Cors Configuration")}
contentSx={{ width: 500 }}
okBtn={loading ? t("Saving...") : t("Save")}
cancelBtn={t("Cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={handleSave}
>
<List sx={{ width: "90%", padding: 2 }}>
<ListItem sx={{ padding: "8px 0", fontWeight: "bold" }}>
{t("External Controller CORS Settings")}
</ListItem>
<ListItem sx={{ padding: "8px 0" }}>
<FormControlLabel
control={
<Switch
checked={corsConfig.allowPrivateNetwork}
onChange={(e) => handleCorsConfigChange("allowPrivateNetwork", e.target.checked)}
/>
}
label={t("Allow private network access")}
/>
</ListItem>
<Divider sx={{ my: 2 }} />
<ListItem sx={{ padding: "8px 0" }}>
<div style={{ width: "100%" }}>
<div style={{ marginBottom: 8, fontWeight: "bold" }}>
{t("Allowed Origins")}
</div>
{corsConfig.allowOrigins.map((origin, index) => (
<div key={index} style={{ display: "flex", alignItems: "center", marginBottom: 8 }}>
<TextField
fullWidth
size="small"
sx={{ fontSize: 14, marginRight: 2 }}
value={origin}
onChange={(e) => handleUpdateOrigin(index, e.target.value)}
placeholder={t("Please enter a valid url")}
inputProps={{ style: { fontSize: 14 } }}
/>
<Button
variant="contained"
color="error"
size="small"
onClick={() => handleDeleteOrigin(index)}
disabled={corsConfig.allowOrigins.length <= 1}
>
<DeleteIcon fontSize="small" />
</Button>
</div>
))}
<Button
variant="contained"
size="small"
onClick={handleAddOrigin}
sx={{ mt: 2 }}
>
{t("Add")}
</Button>
</div>
</ListItem>
</List>
</BaseDialog>
);
}
);

View File

@@ -21,6 +21,7 @@ import { GuardState } from "./mods/guard-state";
import { NetworkInterfaceViewer } from "./mods/network-interface-viewer";
import { SettingItem, SettingList } from "./mods/setting-comp";
import { WebUIViewer } from "./mods/web-ui-viewer";
import { HeaderConfiguration } from "./mods/external-controller-cors";
const isWIN = getSystem() === "windows";
@@ -57,6 +58,7 @@ const SettingClash = ({ onError }: Props) => {
const coreRef = useRef<DialogRef>(null);
const networkRef = useRef<DialogRef>(null);
const dnsRef = useRef<DialogRef>(null);
const corsRef = useRef<DialogRef>(null);
const onSwitchFormat = (_e: any, value: boolean) => value;
const onChangeData = (patch: Partial<IConfigData>) => {
@@ -101,6 +103,7 @@ const SettingClash = ({ onError }: Props) => {
<ClashCoreViewer ref={coreRef} />
<NetworkInterfaceViewer ref={networkRef} />
<DnsViewer ref={dnsRef} />
<HeaderConfiguration ref={corsRef} />
<SettingItem
label={t("Allow Lan")}
@@ -229,6 +232,21 @@ const SettingClash = ({ onError }: Props) => {
}
/>
<SettingItem
onClick={() => corsRef.current?.open()}
label={
<>
{t("External Cors")}
<TooltipIcon
title={t(
"Enable one-click CORS for external API. Click to toggle CORS",
)}
sx={{ opacity: "0.7" }}
/>
</>
}
/>
<SettingItem onClick={() => webRef.current?.open()} label={t("Web UI")} />
<SettingItem

View File

@@ -639,5 +639,13 @@
"AppHiddenTitle": "APP Hidden",
"AppHiddenBody": "APP window hidden by hotkey",
"Invalid Profile URL": "Invalid profile URL. Please enter a URL starting with http:// or https://",
"Saved Successfully": "Saved successfully"
"Saved Successfully": "Saved successfully",
"External Cors": "External Cors",
"Enable one-click CORS for external API. Click to toggle CORS": "Enable one-click CORS for external API. Click to toggle CORS",
"External Cors Configuration": "External Cors Configuration",
"External Controller CORS Settings": "External Controller CORS Settings",
"Allow private network access": "Allow private network access",
"Allowed Origins": "Allowed Origins",
"Please enter a valid url": "Please enter a valid url",
"Add": "Add"
}

View File

@@ -639,5 +639,13 @@
"AppHiddenTitle": "应用隐藏",
"AppHiddenBody": "已通过快捷键隐藏应用窗口",
"Invalid Profile URL": "无效的订阅链接,请输入以 http:// 或 https:// 开头的地址",
"Saved Successfully": "保存成功"
"Saved Successfully": "保存成功",
"External Cors": "外部控制跨域",
"Enable one-click CORS for external API. Click to toggle CORS": "设置内核跨域访问,点击切换 CORS是否启用",
"External Cors Configuration": "外部控制跨域配置",
"External Controller CORS Settings": "外部控制跨域设置",
"Allow private network access": "允许专用网络访问",
"Allowed Origins": "允许的来源",
"Please enter a valid url": "请输入有效的网址",
"Add": "添加"
}

View File

@@ -31,6 +31,10 @@ interface IConfigData {
"socks-port": number;
"tproxy-port": number;
"external-controller": string;
"external-controller-cors": {
"allow-private-network": boolean;
"allow-origins": string[];
};
secret: string;
"unified-delay": boolean;
tun: {