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