add external cors control panel
This commit is contained in:
182
src/components/setting/mods/external-controller-cors.tsx
Normal file
182
src/components/setting/mods/external-controller-cors.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "添加"
|
||||
}
|
||||
|
||||
4
src/services/types.d.ts
vendored
4
src/services/types.d.ts
vendored
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user