diff --git a/package-lock.json b/package-lock.json index a05e094..51586b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,16 +10,20 @@ "dependencies": { "@xyflow/react": "^12.10.0", "base64-js": "^1.5.1", + "class-transformer": "^0.5.1", + "crypto-js": "^4.2.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-hot-toast": "^2.6.0", "react-select": "^5.10.2", "react-switch": "^7.1.0", + "reflect-metadata": "^0.2.2", "tweetnacl": "^1.0.3" }, "devDependencies": { "@eslint/js": "^9.39.1", "@types/base64-js": "^1.3.2", + "@types/crypto-js": "^4.2.2", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", @@ -61,7 +65,6 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -1564,6 +1567,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/crypto-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz", + "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/d3-color": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", @@ -1638,7 +1648,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz", "integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1721,7 +1730,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1861,7 +1869,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -1923,6 +1930,12 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, "node_modules/classcat": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", @@ -2003,12 +2016,17 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/d3-color": { "version": "3.1.0", @@ -2067,7 +2085,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -2236,7 +2253,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3055,7 +3071,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3128,7 +3143,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -3138,7 +3152,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -3229,6 +3242,12 @@ "react-dom": ">=16.6.0" } }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -3524,7 +3543,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -3646,7 +3664,6 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 8a64a53..ff7170c 100644 --- a/package.json +++ b/package.json @@ -12,16 +12,20 @@ "dependencies": { "@xyflow/react": "^12.10.0", "base64-js": "^1.5.1", + "class-transformer": "^0.5.1", + "crypto-js": "^4.2.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-hot-toast": "^2.6.0", "react-select": "^5.10.2", "react-switch": "^7.1.0", + "reflect-metadata": "^0.2.2", "tweetnacl": "^1.0.3" }, "devDependencies": { "@eslint/js": "^9.39.1", "@types/base64-js": "^1.3.2", + "@types/crypto-js": "^4.2.2", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", diff --git a/src/App.tsx b/src/App.tsx index bb1fc8f..e5cc475 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -19,15 +19,19 @@ import { } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import { AppNode, AppEdge, NodeData, EdgeData, NodeDataUpdate, EdgeDataUpdate } from './types/graph'; -import {Settings, initialSettings, SettingsContext} from './types/settings' +import {Settings, SettingsContext} from './types/settings' import CustomNode from './components/CustomNode'; import NodeEditor from './components/NodeEditor'; import EdgeEditor from './components/EdgeEditor' import SettingsEditor from './components/SettingsEditor' import Toggle from "./components/Toggle" import { generateWireGuardPrivateKey } from './utils/wireguardConfig'; +import CryptoJS from 'crypto-js'; import './App.css'; -import { Toaster } from 'react-hot-toast'; +import toast, { Toaster } from 'react-hot-toast'; +import SaveConfig from './types/saveConfig' +import { plainToInstance, instanceToPlain } from 'class-transformer'; + const initialNodes: AppNode[] = []; const initialEdges: AppEdge[] = []; @@ -49,7 +53,7 @@ function generateNodeData(count: number) : NodeData | null { function FlowContent(): ReactNode { const [nodes, setNodes] = useState(initialNodes); const [edges, setEdges] = useState(initialEdges); - const [settings, setSettings] = useState(initialSettings); + const [settings, setSettings] = useState(new Settings()); const [editingNode, setEditingNode] = useState(undefined); const [editingEdge, setEditingEdge] = useState(undefined); @@ -143,6 +147,70 @@ function FlowContent(): ReactNode { ); }; + const handleSaveConfig = async (): Promise => { + try { + const pass = window.prompt('输入用于加密私钥的密码(留空则不加密):'); + const saveConfig: SaveConfig = {settings: settings, nodes: [], edges: [], encrypted: false} + if(pass) saveConfig.encrypted = true; + + nodes.forEach(node => { + const nodeData = node.data; + let privateKey = nodeData.privateKey; + if(pass) privateKey = CryptoJS.AES.encrypt(privateKey, pass).toString(); + saveConfig.nodes.push({...node, data: {...nodeData, privateKey: privateKey}}); + }); + + edges.forEach(edge => { + saveConfig.edges.push(edge); + }); + + const json = instanceToPlain(saveConfig); + const blob = new Blob([JSON.stringify(json, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'wg-config.json'; + a.click(); + URL.revokeObjectURL(url); + } catch (e) { + toast.error('保存失败: ' + e); + } + }; + + const handleLoadConfig = async (): Promise => { + try { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = 'application/json'; + input.onchange = async () => { + const file = input.files?.[0]; + if (!file) return; + const jsonString = await file.text(); + const plainObject = JSON.parse(jsonString); + const saveConfig: SaveConfig = plainToInstance(SaveConfig, plainObject); + + if (saveConfig.encrypted) { + const pass = window.prompt('输入用于解密私钥的密码:'); + if (!pass) { toast.error('需要密码以解密私钥'); return; } + try { + for(let node of saveConfig.nodes) { + node.data.privateKey = CryptoJS.AES.decrypt(node.data.privateKey, pass).toString(CryptoJS.enc.Utf8); + } + } catch (err) { + toast.error('解密失败: ' + err); + return; + } + } + setSettings(saveConfig.settings); + setNodes(saveConfig.nodes); + setEdges(saveConfig.edges); + }; + input.click(); + } catch (e) { + toast.error('加载失败: ' + e); + } + }; + return (
@@ -188,6 +256,14 @@ function FlowContent(): ReactNode { 📋 设置 + + + +
diff --git a/src/main.tsx b/src/main.tsx index 5d0c99c..0ac409f 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,3 +1,4 @@ +import 'reflect-metadata'; import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' diff --git a/src/types/saveConfig.ts b/src/types/saveConfig.ts new file mode 100644 index 0000000..4767183 --- /dev/null +++ b/src/types/saveConfig.ts @@ -0,0 +1,14 @@ +import { Type } from 'class-transformer'; +import { Settings } from './settings'; +import { AppNode, AppEdge } from './graph'; + +export default class SaveConfig { + encrypted: boolean = false; + + @Type(() => Settings) + settings: Settings = new Settings(); + + nodes: AppNode[] = []; + + edges: AppEdge[] = []; +} \ No newline at end of file diff --git a/src/types/settings.ts b/src/types/settings.ts index 19709e6..513fab9 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -1,22 +1,28 @@ +import { Type } from 'class-transformer'; import { createContext } from 'react'; import { CIDR } from '../utils/iputils'; -export interface SubnetInfo { - subnet: CIDR; - nodes: Array<{nodeId: string, cidr: CIDR | undefined}>; +export class NodeToCIDR { + nodeId: string = ''; + + @Type(() => CIDR) // 必须显式标记,即使是 undefined 也要处理 + cidr: CIDR | undefined; } -export interface Settings { - listenPort: number; - mtu: number; +export class SubnetInfo { + @Type(() => CIDR) + subnet!: CIDR; - subnets: SubnetInfo[]; + @Type(() => NodeToCIDR) + nodes: NodeToCIDR[] = []; } -export const initialSettings : Settings = { - listenPort: 38894, - mtu: 1420, - subnets: [] -}; +export class Settings { + listenPort: number = 38894; + mtu: number = 1420; -export const SettingsContext = createContext(initialSettings); + @Type(() => SubnetInfo) + subnets: SubnetInfo[] = []; +} + +export const SettingsContext = createContext(new Settings()); diff --git a/tsconfig.json b/tsconfig.json index c19f560..30b402e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,7 +20,9 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "esModuleInterop": true, - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }]