initial commit
This commit is contained in:
31
.eslintrc.json
Executable file
31
.eslintrc.json
Executable file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"plugins": ["@typescript-eslint", "react", "react-hooks"],
|
||||
"rules": {
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"@typescript-eslint/no-explicit-any": "warn"
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"es2021": true
|
||||
}
|
||||
}
|
||||
47
.gitignore
vendored
Executable file
47
.gitignore
vendored
Executable file
@@ -0,0 +1,47 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
dist-electron/
|
||||
|
||||
# Electron builder outputs
|
||||
*.deb
|
||||
*.AppImage
|
||||
*.snap
|
||||
*.dmg
|
||||
*.exe
|
||||
|
||||
# Development
|
||||
.vite/
|
||||
.electron-vite/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
|
||||
# Misc
|
||||
.cache/
|
||||
3
.prettierignore
Executable file
3
.prettierignore
Executable file
@@ -0,0 +1,3 @@
|
||||
.DS_Store
|
||||
/node_modules/
|
||||
/dist/
|
||||
7
.prettierrc
Executable file
7
.prettierrc
Executable file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 80
|
||||
}
|
||||
15
LICENSE
Executable file
15
LICENSE
Executable file
@@ -0,0 +1,15 @@
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2025 Zerostate
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
53
README.md
Executable file
53
README.md
Executable file
@@ -0,0 +1,53 @@
|
||||
# Dark Shield
|
||||
|
||||
A simple, lightweight GUI for managing WireGuard VPN connections on Linux.
|
||||
|
||||
<img src="./imgs/screenshot.png" width="175" height="325" />
|
||||
|
||||
## Features
|
||||
|
||||
- Load WireGuard `.conf` files
|
||||
- Connect/disconnect with one click
|
||||
- Real-time connection status monitoring
|
||||
- Clean, minimal dark mode interface
|
||||
|
||||
## Requirements
|
||||
|
||||
- Linux (Debian-based distributions)
|
||||
- WireGuard Tools (`wireguard-tools`)
|
||||
|
||||
## Installation
|
||||
|
||||
Download the `.deb` package from releases and install:
|
||||
|
||||
```bash
|
||||
sudo dpkg -i dark-shield_*.deb
|
||||
```
|
||||
|
||||
The package automatically configures passwordless WireGuard operations for users in the `sudo` group.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Launch Dark Shield
|
||||
2. Click "Load Config" and select your `.conf` file
|
||||
3. Click "Connect" to establish the VPN connection
|
||||
4. Click "Disconnect" when done
|
||||
|
||||
The app stores your last loaded config and will remember it between sessions.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
yarn install
|
||||
|
||||
# Run in development mode
|
||||
yarn dev
|
||||
|
||||
# Build .deb package
|
||||
yarn build:deb
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
ISC License - see LICENSE file for details.
|
||||
10
build/post-install.sh
Executable file
10
build/post-install.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copy sudoers rule from app resources to system location
|
||||
cp /opt/Dark\ Shield/resources/sudoers.d/dark-shield /etc/sudoers.d/dark-shield
|
||||
|
||||
# Set proper permissions for sudoers file
|
||||
sudo chmod 0440 /etc/sudoers.d/dark-shield
|
||||
chown root:root /etc/sudoers.d/dark-shield
|
||||
|
||||
exit 0
|
||||
175
electron/main.ts
Executable file
175
electron/main.ts
Executable file
@@ -0,0 +1,175 @@
|
||||
import { app, BrowserWindow, ipcMain, dialog } from 'electron';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
const CONFIG_DIR = path.join(app.getPath('home'), '.config', 'wireguard-gui');
|
||||
const CONFIG_FILE = path.join(CONFIG_DIR, 'current.conf');
|
||||
const WG_INTERFACE = 'wg0';
|
||||
const WG_CONF_PATH = `/etc/wireguard/${WG_INTERFACE}.conf`;
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
|
||||
function createWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 350,
|
||||
height: 650,
|
||||
resizable: false,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
devTools: process.env.NODE_ENV === 'development',
|
||||
},
|
||||
});
|
||||
|
||||
// Remove menu bar completely
|
||||
mainWindow.setMenuBarVisibility(false);
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
mainWindow.loadURL('http://localhost:5173');
|
||||
} else {
|
||||
mainWindow.loadFile(path.join(__dirname, '../dist/index.html'));
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure config directory exists
|
||||
function ensureConfigDir() {
|
||||
if (!fs.existsSync(CONFIG_DIR)) {
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
// IPC Handlers
|
||||
ipcMain.handle('select-config-file', async () => {
|
||||
const result = await dialog.showOpenDialog(mainWindow!, {
|
||||
properties: ['openFile'],
|
||||
filters: [{ name: 'WireGuard Config', extensions: ['conf'] }],
|
||||
});
|
||||
|
||||
if (result.canceled || result.filePaths.length === 0) {
|
||||
return { success: false, error: 'No file selected' };
|
||||
}
|
||||
|
||||
return { success: true, filePath: result.filePaths[0] };
|
||||
});
|
||||
|
||||
ipcMain.handle('load-config', async (_event, filePath: string) => {
|
||||
try {
|
||||
ensureConfigDir();
|
||||
|
||||
// Copy config to our storage
|
||||
fs.copyFileSync(filePath, CONFIG_FILE);
|
||||
|
||||
// Create symlink with sudo (non-interactive)
|
||||
const symlinkCmd = `sudo -n ln -sf ${CONFIG_FILE} ${WG_CONF_PATH}`;
|
||||
await execAsync(symlinkCmd);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
configName: path.basename(filePath),
|
||||
message: 'Config loaded successfully',
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || 'Failed to load config',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('connect-wireguard', async () => {
|
||||
try {
|
||||
const cmd = `sudo -n wg-quick up ${WG_INTERFACE}`;
|
||||
const { stdout, stderr } = await execAsync(cmd);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Connected successfully',
|
||||
output: stdout || stderr,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || 'Failed to connect',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('disconnect-wireguard', async () => {
|
||||
try {
|
||||
const cmd = `sudo -n wg-quick down ${WG_INTERFACE}`;
|
||||
const { stdout, stderr } = await execAsync(cmd);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Disconnected successfully',
|
||||
output: stdout || stderr,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || 'Failed to disconnect',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-connection-status', async () => {
|
||||
try {
|
||||
// Check if interface exists without requiring root
|
||||
await execAsync(`ip link show ${WG_INTERFACE}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
connected: true,
|
||||
details: `${WG_INTERFACE} is up`,
|
||||
};
|
||||
} catch {
|
||||
// ip link returns error if interface doesn't exist
|
||||
return {
|
||||
success: true,
|
||||
connected: false,
|
||||
details: '',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-current-config', async () => {
|
||||
try {
|
||||
if (fs.existsSync(CONFIG_FILE)) {
|
||||
const configName = 'current.conf';
|
||||
return {
|
||||
success: true,
|
||||
hasConfig: true,
|
||||
configName,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
hasConfig: false,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
app.whenReady().then(createWindow);
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
10
electron/preload.ts
Executable file
10
electron/preload.ts
Executable file
@@ -0,0 +1,10 @@
|
||||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
selectConfigFile: () => ipcRenderer.invoke('select-config-file'),
|
||||
loadConfig: (filePath: string) => ipcRenderer.invoke('load-config', filePath),
|
||||
connectWireguard: () => ipcRenderer.invoke('connect-wireguard'),
|
||||
disconnectWireguard: () => ipcRenderer.invoke('disconnect-wireguard'),
|
||||
getConnectionStatus: () => ipcRenderer.invoke('get-connection-status'),
|
||||
getCurrentConfig: () => ipcRenderer.invoke('get-current-config'),
|
||||
});
|
||||
BIN
icons/icon.png
Executable file
BIN
icons/icon.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
BIN
imgs/screenshot.png
Executable file
BIN
imgs/screenshot.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
11
index.html
Executable file
11
index.html
Executable file
@@ -0,0 +1,11 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Dark Shield</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
88
package.json
Executable file
88
package.json
Executable file
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"name": "dark-shield",
|
||||
"version": "1.0.0",
|
||||
"description": "WireGuard GUI Manager",
|
||||
"main": "dist-electron/main.js",
|
||||
"author": {
|
||||
"name": "zerostate",
|
||||
"email": "zerostate@shadowvault.eu"
|
||||
},
|
||||
"homepage": "shadowvault.eu",
|
||||
"license": "ISC",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -p tsconfig.json && vite build",
|
||||
"build:deb": "npm run build && electron-builder --linux deb",
|
||||
"preview": "vite preview",
|
||||
"type-check": "tsc --noEmit",
|
||||
"prepare": "simple-git-hooks"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^3.0.0",
|
||||
"@chakra-ui/system": "^2.6.2",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@tanstack/react-query": "^5.0.0",
|
||||
"framer-motion": "^12.23.24",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.6",
|
||||
"@types/react": "^18.2.46",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@typescript-eslint/eslint-plugin": "^8.46.2",
|
||||
"@typescript-eslint/parser": "^8.46.2",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"electron": "^28.1.0",
|
||||
"electron-builder": "^24.9.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"lint-staged": "^15.2.0",
|
||||
"prettier": "^3.1.1",
|
||||
"simple-git-hooks": "^2.9.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.10",
|
||||
"vite-plugin-electron": "^0.28.0",
|
||||
"vite-plugin-electron-renderer": "^0.14.5"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "npx lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{mjs,js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint"
|
||||
],
|
||||
"*.{css,less,scss,json,graphql}": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"build": {
|
||||
"appId": "eu.shadowvault.zerostate.dark-shield",
|
||||
"productName": "Dark Shield",
|
||||
"linux": {
|
||||
"target": [
|
||||
"deb"
|
||||
],
|
||||
"category": "Network"
|
||||
},
|
||||
"deb": {
|
||||
"depends": [
|
||||
"wireguard-tools"
|
||||
],
|
||||
"afterInstall": "build/post-install.sh"
|
||||
},
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "sudoers.d/dark-shield",
|
||||
"to": "sudoers.d/dark-shield"
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
"dist/**/*",
|
||||
"dist-electron/**/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
156
src/App.tsx
Executable file
156
src/App.tsx
Executable file
@@ -0,0 +1,156 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Box, VStack, Button, Text, Badge, Image } from '@chakra-ui/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { ConnectionControls } from './components/ConnectControls';
|
||||
import { DarkMode } from '@chakra-ui/system';
|
||||
// @ts-expect-error
|
||||
import shield from './icons/icon.png';
|
||||
|
||||
function App() {
|
||||
const [configName, setConfigName] = useState<string>('');
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
// Poll connection status every 3 seconds
|
||||
const { data: connectionStatus } = useQuery({
|
||||
queryKey: ['connectionStatus'],
|
||||
queryFn: async () => {
|
||||
const result = await window.electronAPI.getConnectionStatus();
|
||||
return result.success && result.connected ? result.connected : false;
|
||||
},
|
||||
refetchInterval: 3000,
|
||||
initialData: false,
|
||||
});
|
||||
|
||||
const checkCurrentConfig = async () => {
|
||||
const result = await window.electronAPI.getCurrentConfig();
|
||||
if (result.success && result.hasConfig && result.configName) {
|
||||
setConfigName(result.configName);
|
||||
}
|
||||
};
|
||||
|
||||
// Check for existing config on mount
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line
|
||||
checkCurrentConfig();
|
||||
}, []);
|
||||
|
||||
const handleLoadConfig = async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
const selectResult = await window.electronAPI.selectConfigFile();
|
||||
|
||||
if (!selectResult.success || !selectResult.filePath) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const loadResult = await window.electronAPI.loadConfig(
|
||||
selectResult.filePath
|
||||
);
|
||||
|
||||
if (loadResult.success && loadResult.configName) {
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 2000);
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DarkMode>
|
||||
<Box
|
||||
minH="100vh"
|
||||
bg="gray.600"
|
||||
color="gray.600"
|
||||
p={6}
|
||||
borderTop="none"
|
||||
borderWidth="1px"
|
||||
borderColor="gray.600"
|
||||
>
|
||||
<VStack gap={4} align="stretch">
|
||||
<Image h={75} w={75} margin="auto" src={shield} />
|
||||
|
||||
<Text
|
||||
color="gray.300"
|
||||
fontSize="3xl"
|
||||
fontWeight="bold"
|
||||
textAlign="center"
|
||||
>
|
||||
Dark Shield
|
||||
</Text>
|
||||
|
||||
<Box
|
||||
p={4}
|
||||
bg="gray.300"
|
||||
borderRadius="md"
|
||||
borderWidth="1px"
|
||||
borderColor="gray.600"
|
||||
shadow="sm"
|
||||
>
|
||||
<VStack gap={3} align="stretch">
|
||||
<Text fontSize="sm" color="gray.800" fontWeight="semibold">
|
||||
Connection Status
|
||||
</Text>
|
||||
<Badge
|
||||
bg={connectionStatus ? 'green.50' : 'red.50'}
|
||||
borderRadius="md"
|
||||
borderWidth="1px"
|
||||
borderColor={connectionStatus ? 'green.300' : 'red.300'}
|
||||
color={connectionStatus ? 'green.700' : 'red.700'}
|
||||
fontSize="md"
|
||||
p={2}
|
||||
textAlign="center"
|
||||
>
|
||||
{connectionStatus ? 'Connected' : 'Disconnected'}
|
||||
</Badge>
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
p={4}
|
||||
bg="gray.300"
|
||||
borderRadius="md"
|
||||
borderWidth="1px"
|
||||
borderColor="gray.600"
|
||||
shadow="sm"
|
||||
>
|
||||
<VStack gap={3} align="stretch">
|
||||
<Text fontSize="sm" color="gray.800" fontWeight="semibold">
|
||||
Configuration
|
||||
</Text>
|
||||
{configName ? (
|
||||
<Text fontSize="md" color="gray.800" fontWeight="medium">
|
||||
{configName}
|
||||
</Text>
|
||||
) : (
|
||||
<Text fontSize="sm" color="gray.800">
|
||||
No config loaded
|
||||
</Text>
|
||||
)}
|
||||
<Button
|
||||
bg="gray.800"
|
||||
color="white"
|
||||
_hover={{ bg: 'gray.700' }}
|
||||
onClick={handleLoadConfig}
|
||||
loading={isLoading}
|
||||
loadingText="Loading..."
|
||||
>
|
||||
Load Config
|
||||
</Button>
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
<ConnectionControls
|
||||
isConnected={connectionStatus}
|
||||
hasConfig={!!configName}
|
||||
onConnectionChange={() => {}}
|
||||
/>
|
||||
</VStack>
|
||||
</Box>
|
||||
</DarkMode>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
94
src/components/ConnectControls.tsx
Executable file
94
src/components/ConnectControls.tsx
Executable file
@@ -0,0 +1,94 @@
|
||||
import { useState } from 'react';
|
||||
import { Box, VStack, Button, Text } from '@chakra-ui/react';
|
||||
|
||||
interface ConnectionControlsProps {
|
||||
isConnected: boolean;
|
||||
hasConfig: boolean;
|
||||
onConnectionChange: (connected: boolean) => void;
|
||||
}
|
||||
|
||||
export function ConnectionControls({
|
||||
isConnected,
|
||||
hasConfig,
|
||||
onConnectionChange,
|
||||
}: ConnectionControlsProps) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleConnect = async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
const result = await window.electronAPI.connectWireguard();
|
||||
|
||||
if (result.success) {
|
||||
onConnectionChange(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 2000);
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisconnect = async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
const result = await window.electronAPI.disconnectWireguard();
|
||||
|
||||
if (result.success) {
|
||||
onConnectionChange(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 2000);
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
p={4}
|
||||
bg="gray.300"
|
||||
borderRadius="md"
|
||||
borderWidth="1px"
|
||||
borderColor="gray.600"
|
||||
shadow="sm"
|
||||
>
|
||||
<VStack gap={3} align="stretch">
|
||||
<Text fontSize="sm" color="gray.800" fontWeight="semibold">
|
||||
Connection Controls
|
||||
</Text>
|
||||
{!hasConfig && (
|
||||
<Text fontSize="sm" color="orange.600">
|
||||
Please load a config file first
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{hasConfig && isConnected ? (
|
||||
<Button
|
||||
bg="red.600"
|
||||
color="white"
|
||||
_hover={{ bg: 'red.700' }}
|
||||
onClick={handleDisconnect}
|
||||
disabled={isLoading}
|
||||
loading={isLoading}
|
||||
>
|
||||
Disconnect
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
bg="green.600"
|
||||
color="white"
|
||||
_hover={{ bg: 'green.700' }}
|
||||
onClick={handleConnect}
|
||||
disabled={isLoading}
|
||||
loading={isLoading}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
)}
|
||||
</VStack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
BIN
src/icons/icon.png
Executable file
BIN
src/icons/icon.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
21
src/main.tsx
Executable file
21
src/main.tsx
Executable file
@@ -0,0 +1,21 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { ChakraProvider, defaultSystem } from '@chakra-ui/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import App from './App';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: 2,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<ChakraProvider value={defaultSystem}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
</QueryClientProvider>
|
||||
</ChakraProvider>
|
||||
);
|
||||
52
src/vite.env.d.ts
vendored
Executable file
52
src/vite.env.d.ts
vendored
Executable file
@@ -0,0 +1,52 @@
|
||||
declare module '*.png' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.jpg' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.svg' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
export interface ElectronAPI {
|
||||
getConnectionStatus: () => Promise<{
|
||||
success: boolean;
|
||||
connected?: boolean;
|
||||
}>;
|
||||
getCurrentConfig: () => Promise<{
|
||||
success: boolean;
|
||||
hasConfig?: boolean;
|
||||
configName?: string;
|
||||
}>;
|
||||
selectConfigFile: () => Promise<{
|
||||
success: boolean;
|
||||
filePath?: string;
|
||||
}>;
|
||||
loadConfig: (filePath: string) => Promise<{
|
||||
success: boolean;
|
||||
configName?: string;
|
||||
}>;
|
||||
connectWireguard: () => Promise<{
|
||||
success: boolean;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
disconnectWireguard: () => Promise<{
|
||||
success: boolean;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electronAPI: ElectronAPI;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
4
sudoers.d/dark-shield
Executable file
4
sudoers.d/dark-shield
Executable file
@@ -0,0 +1,4 @@
|
||||
# Allow users to run WireGuard commands without password
|
||||
%sudo ALL=(ALL) NOPASSWD: /usr/bin/wg-quick up wg0
|
||||
%sudo ALL=(ALL) NOPASSWD: /usr/bin/wg-quick down wg0
|
||||
%sudo ALL=(ALL) NOPASSWD: /usr/bin/ln -sf * /etc/wireguard/wg0.conf
|
||||
8
tsconfig.app.json
Executable file
8
tsconfig.app.json
Executable file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
16
tsconfig.json
Executable file
16
tsconfig.json
Executable file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"resolveJsonModule": true,
|
||||
"outDir": "./dist",
|
||||
"typeRoots": ["./src/types", "./node_modules/@types"]
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
"include": ["electron/**/*", "src"]
|
||||
}
|
||||
27
vite.config.ts
Executable file
27
vite.config.ts
Executable file
@@ -0,0 +1,27 @@
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { defineConfig } from 'vite';
|
||||
import electron from 'vite-plugin-electron';
|
||||
import renderer from 'vite-plugin-electron-renderer';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
electron([
|
||||
{
|
||||
entry: 'electron/main.ts',
|
||||
},
|
||||
{
|
||||
entry: 'electron/preload.ts',
|
||||
onstart(options: { reload: () => void }) {
|
||||
options.reload();
|
||||
},
|
||||
},
|
||||
]),
|
||||
renderer(),
|
||||
],
|
||||
base: './',
|
||||
server: {
|
||||
port: 5173,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user