diff --git a/package.json b/package.json index aeb2cc7..d4a31c0 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,14 @@ { "from": "src/assets", "to": "assets" + }, + { + "from": "resources/wireguard-install", + "to": "wireguard-install" + }, + { + "from": "resources/wireguard", + "to": "wireguard" } ], "mac": { @@ -83,6 +91,7 @@ } }, "win": { + "requestedExecutionLevel": "requireAdministrator", "target": [ { "target": "nsis", @@ -126,4 +135,4 @@ "vite": "^5.4.7", "vitest": "^2.1.1" } -} +} \ No newline at end of file diff --git a/resources/wireguard-install/wireguard-installer.exe b/resources/wireguard-install/wireguard-installer.exe new file mode 100644 index 0000000..032922d Binary files /dev/null and b/resources/wireguard-install/wireguard-installer.exe differ diff --git a/resources/wireguard/mac/wg0.conf b/resources/wireguard/mac/wg0.conf new file mode 100644 index 0000000..5053efb --- /dev/null +++ b/resources/wireguard/mac/wg0.conf @@ -0,0 +1,10 @@ +[Interface] +PrivateKey = CEzCd25ufxbOjvRhMqLfkcdP/OziG5i6OkXCfMZrI1I= +Address = 10.0.0.2/24 +DNS = 1.1.1.1 + +[Peer] +PublicKey = jG+7H5thEiNjxY1XqI1R9oyMAMTIegLwE3tZkS1Rj2U= +Endpoint = 35.95.209.161:51820 +AllowedIPs = 10.0.0.0/24 +PersistentKeepalive = 25 diff --git a/resources/wireguard/windows/wg0.conf b/resources/wireguard/windows/wg0.conf new file mode 100644 index 0000000..5053efb --- /dev/null +++ b/resources/wireguard/windows/wg0.conf @@ -0,0 +1,10 @@ +[Interface] +PrivateKey = CEzCd25ufxbOjvRhMqLfkcdP/OziG5i6OkXCfMZrI1I= +Address = 10.0.0.2/24 +DNS = 1.1.1.1 + +[Peer] +PublicKey = jG+7H5thEiNjxY1XqI1R9oyMAMTIegLwE3tZkS1Rj2U= +Endpoint = 35.95.209.161:51820 +AllowedIPs = 10.0.0.0/24 +PersistentKeepalive = 25 diff --git a/src/electron/ipc/ipcHandler.ts b/src/electron/ipc/ipcHandler.ts index edaa1df..5ec569b 100644 --- a/src/electron/ipc/ipcHandler.ts +++ b/src/electron/ipc/ipcHandler.ts @@ -3,6 +3,7 @@ import { getToken, saveToken, clearToken } from "../utils/tokenHelper.js"; import { getActiveApp, handleLogin, takeScreenshot, startWindowPolling, getRunningAppsList, captureSpecificApp } from "../utils/utilityFunctions.js"; import api from "../api/axiosClient.js"; import { getMainWindow } from '../main.js'; +import { startVPN, stopVPN } from "../vpn.js"; @@ -546,4 +547,10 @@ export const sendTokenExpired = () => { }; -; +ipcMain.handle("vpn-connect", async () => { + return startVPN(); +}); + +ipcMain.handle("vpn-disconnect", async () => { + stopVPN(); +}); \ No newline at end of file diff --git a/src/electron/main.ts b/src/electron/main.ts index 2e6f53d..9dbe4d0 100644 --- a/src/electron/main.ts +++ b/src/electron/main.ts @@ -8,6 +8,9 @@ import "./ipc/ipcHandler.js"; import path from "path"; import { fileURLToPath } from "url"; +import * as fs from "fs"; +import { execFile } from "child_process"; + app.setAppUserModelId("com.tracker.app"); const __filename = fileURLToPath(import.meta.url); @@ -52,12 +55,26 @@ app.on("second-instance", () => { } }); + +// CHECKING IF WIREGUARD EXIST - IF NOT INSTALLING IN CLIENT MACHINE +const wgPath = "C:\\Program Files\\WireGuard\\wireguard.exe"; + +if (!fs.existsSync(wgPath)) { + const installer = path.join(process.resourcesPath, "wireguard-install", "wireguard-installer.exe"); + + execFile(installer, ["/quiet"], (err) => { + if (err) console.error("WireGuard install failed:", err); + else console.log("WireGuard installed successfully."); + }); +} +// CHECKING IF WIREGUARD EXIST - IF NOT INSTALLING IN CLIENT MACHINE + + // --------------------------- // MAIN APP READY // --------------------------- app.on("ready", async () => { - splashWindow = new BrowserWindow({ width: 300, height: 300, @@ -90,8 +107,8 @@ app.on("ready", async () => { // alwaysOnTop: true, // skipTaskbar: true, autoHideMenuBar: true, - frame: true, - show: false, + frame: true, + show: false, icon: resolveIcon(), webPreferences: { devTools: true, @@ -102,14 +119,14 @@ app.on("ready", async () => { }); - - // win.setAlwaysOnTop(true, 'screen-saver'); - // win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); - // win.setFullScreenable(false); - + + // win.setAlwaysOnTop(true, 'screen-saver'); + // win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); + // win.setFullScreenable(false); + // Cross-platform safe always-on-top const isMac = process.platform === "darwin"; - + if (isMac) { win.setAlwaysOnTop(true, "floating"); // best for macOS win.setVisibleOnAllWorkspaces(true); // NO fullscreen override @@ -117,7 +134,7 @@ app.on("ready", async () => { win.setAlwaysOnTop(true, "normal"); // safest for Windows + Zoom win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); win.setFullScreenable(false); -} + } mainWindow = win; diff --git a/src/electron/preload.cts b/src/electron/preload.cts index 870cf71..1de0eea 100644 --- a/src/electron/preload.cts +++ b/src/electron/preload.cts @@ -65,6 +65,9 @@ contextBridge.exposeInMainWorld('electron', { getLeaveList: () => ipcRenderer.invoke("get-leave-list"), getAdjustmentsList:(vaId: string) => ipcRenderer.invoke("get-adjustments-list",vaId), + connectVPN: () => ipcRenderer.invoke("vpn-connect"), + disconnectVPN: () => ipcRenderer.invoke("vpn-disconnect"), + } satisfies Window['electron']); // function ipcInvoke( diff --git a/src/electron/utils/utilityFunctions.ts b/src/electron/utils/utilityFunctions.ts index 17d3319..c09a7ab 100644 --- a/src/electron/utils/utilityFunctions.ts +++ b/src/electron/utils/utilityFunctions.ts @@ -68,7 +68,7 @@ export const handleLogin = async (credentials: { email: string, password: string try { console.log("➡️ Calling API..."); const response = await axios.post( - 'quality-friendly-goldfish.ngrok-free.app/api/v1/va/login', + 'https://quality-friendly-goldfish.ngrok-free.app/api/v1/va/login', { userDetails: { ...credentials } }, { headers: { "ngrok-skip-browser-warning": 69420 }, httpsAgent } ); diff --git a/src/electron/vpn.ts b/src/electron/vpn.ts new file mode 100644 index 0000000..19596b9 --- /dev/null +++ b/src/electron/vpn.ts @@ -0,0 +1,223 @@ +import { app } from "electron"; +import { exec, execFile } from "child_process"; +import fs from "fs"; +import os from "os"; +import path from "path"; + +// +// ------------------ HELPERS ------------------ +// + +// Promise wrapper for exec() +function execPromise(cmd: string): Promise { + return new Promise((resolve, reject) => { + exec(cmd, (err, stdout, stderr) => { + if (err) return reject(stderr || err); + resolve(stdout); + }); + }); +} + +// Detect dev vs production +function isDev() { + return !app.isPackaged; +} + +// +// ------------------ PATH RESOLVERS ------------------ +// + +// WireGuard installer path (Windows only) +// function getWireGuardInstallerPath(): string { +// return isDev() +// ? path.join(process.cwd(), "resources", "wireguard-install", "wireguard-installer.exe") +// : path.join(process.resourcesPath, "wireguard-install", "wireguard-installer.exe"); +// } + +function getWireGuardInstallerPath(): string { + const devPath = path.join(process.cwd(), "resources", "wireguard-install", "wireguard-installer.exe"); + + // When running "npm run dev", use devPath ALWAYS + if (!app.isPackaged) { + console.log("DEV MODE Installer Path:", devPath); + return devPath; + } + + // Production build path + const prodPath = path.join(process.resourcesPath, "wireguard-install", "wireguard-installer.exe"); + console.log("PROD MODE Installer Path:", prodPath); + return prodPath; +} + + +// OS-specific wg0.conf +function getConfigPath(): string { + const base = isDev() + ? path.join(process.cwd(), "resources", "wireguard") + : path.join(process.resourcesPath, "wireguard"); + + if (process.platform === "win32") { + return path.join(base, "windows", "wg0.conf"); + } + + if (process.platform === "darwin") { + return path.join(base, "macos", "wg0.conf"); + } + + throw new Error("Unsupported OS"); +} + +// Detect installed WireGuard.exe on Windows +function findWireGuardCLI(): string | null { + const possiblePaths = [ + "C:\\Program Files\\WireGuard\\wireguard.exe", + "C:\\Program Files (x86)\\WireGuard\\wireguard.exe" + ]; + + for (const p of possiblePaths) { + if (fs.existsSync(p)) return p; + } + + return null; +} + +// +// ------------------ WINDOWS INSTALL LOGIC ------------------ +// + +// Install WireGuard silently +async function ensureWireGuardInstalled(): Promise { + let cli = findWireGuardCLI(); + if (cli) return cli; + + // Installer path + const installer = getWireGuardInstallerPath(); + if (!fs.existsSync(installer)) { + throw new Error("WireGuard installer not found in resources."); + } + + console.log("WireGuard not found. Installing silently..."); + await execPromise(`"${installer}" /quiet`); + + cli = findWireGuardCLI(); + if (!cli) throw new Error("WireGuard installation failed."); + + console.log("WireGuard installed successfully:", cli); + return cli; +} + +// +// ------------------ WINDOWS CONNECT/DISCONNECT ------------------ +// + +// async function connectWindows(): Promise { +// const cli = await ensureWireGuardInstalled(); +// const conf = getConfigPath(); + +// // Disable GUI auto popup +// await execPromise( +// `reg add "HKCU\\Software\\WireGuard" /v SuppressTray /t REG_DWORD /d 1 /f` +// ); + +// return new Promise((resolve, reject) => { +// execFile(cli, ["/installtunnelservice", conf], (err) => { +// if (err) return reject("Failed to connect VPN: " + err); +// resolve("Windows VPN connected without GUI"); +// }); +// }); +// } + +// let wgRuntimeProcess: any = null; + +// async function connectWindows(): Promise { +// const cli = await ensureWireGuardInstalled(); +// const conf = getConfigPath(); + +// return new Promise((resolve, reject) => { +// wgRuntimeProcess = execFile(cli, ["/tunnelservice", conf], (err) => { +// if (err) return reject("VPN connection error: " + err); +// }); + +// // Wait a bit for interface to come up +// setTimeout(() => resolve("Windows VPN connected silently"), 800); +// }); +// } + + +async function connectWindows(): Promise { + const cli = await ensureWireGuardInstalled(); + const conf = getConfigPath(); + + // Disable GUI popup and silent mode (ignore errors) + await execPromise(`reg add "HKCU\\Software\\WireGuard" /v HideTrayIcon /t REG_DWORD /d 1 /f`).catch(() => {}); + await execPromise(`reg add "HKCU\\Software\\WireGuard" /v SuppressTray /t REG_DWORD /d 1 /f`).catch(() => {}); + await execPromise(`reg delete "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" /v WireGuard /f`).catch(() => {}); + + // Start background service + await execPromise(`sc start WireGuardManager`).catch(() => {}); + + return new Promise((resolve, reject) => { + execFile(cli, ["/installtunnelservice", conf], (err) => { + if (err) return reject("VPN connection failed: " + err); + resolve("Windows VPN connected silently"); + }); + }); +} + +// function disconnectWindows(): Promise { +// const cli = findWireGuardCLI(); +// if (!cli) return Promise.reject("WireGuard CLI not found"); + +// return execPromise(`"${cli}" /uninstalltunnelservice wg0`); +// } + +async function disconnectWindows(): Promise { + const cli = findWireGuardCLI(); + if (!cli) throw new Error("WireGuard CLI not found"); + + // 1. Remove the installed tunnel service + await execPromise(`"${cli}" /uninstalltunnelservice wg0`); + + // 2. Ensure background service is still running (optional clean-up) + // Windows sometimes marks the service as "stopped" after uninstall + await execPromise(`sc stop WireGuardTunnel$wg0`).catch(() => { }); + await execPromise(`sc stop WireGuardManager`).catch(() => { }); + + return "Windows VPN disconnected"; +} + + +// +// ------------------ MACOS CONNECT/DISCONNECT ------------------ +// + +function connectMac(): Promise { + const conf = getConfigPath(); + return execPromise(`sudo wg-quick up "${conf}"`).then(() => "macOS VPN connected"); +} + +function disconnectMac(): Promise { + return execPromise(`sudo wg-quick down wg0`).then(() => "macOS VPN disconnected"); +} + +// +// ------------------ PUBLIC API ------------------ +// + +export function startVPN(): Promise { + const platform = os.platform(); + + if (platform === "win32") return connectWindows(); + if (platform === "darwin") return connectMac(); + + return Promise.reject("Unsupported OS"); +} + +export function stopVPN(): Promise { + const platform = os.platform(); + + if (platform === "win32") return disconnectWindows(); + if (platform === "darwin") return disconnectMac(); + + return Promise.reject("Unsupported OS"); +} diff --git a/src/ui/layout/DashboardLayout.tsx b/src/ui/layout/DashboardLayout.tsx index d1142e4..fc02701 100644 --- a/src/ui/layout/DashboardLayout.tsx +++ b/src/ui/layout/DashboardLayout.tsx @@ -260,11 +260,11 @@ const DashboardLayout = () => { ); // STEP 6: Update UI (restore window) - console.log("🪟 STEP 6: Restoring window..."); - await window.electron.toggleCloseBtn(true); - await window.electron.resizeWindow(500, 600); - toggleWindowSize.current = true; - console.log("✅ Window restored"); + console.log("🪟 STEP 6: Restoring window..."); + await window.electron.toggleCloseBtn(true); + await window.electron.resizeWindow(500, 600); + toggleWindowSize.current = true; + console.log("✅ Window restored"); // Allow React to process the state update await new Promise(resolve => setTimeout(resolve, 50)); @@ -331,7 +331,7 @@ const DashboardLayout = () => { } } - + } catch (error) { console.error("❌ Error refreshing project list:", error); @@ -588,7 +588,7 @@ const DashboardLayout = () => { if (!selectedProject?.projectId) return console.log(selectedProject?.projectId, "SelectedProject ID") - socketRef.current = io("quality-friendly-goldfish.ngrok-free.app", { + socketRef.current = io("https://quality-friendly-goldfish.ngrok-free.app", { transports: ["websocket"], secure: true, rejectUnauthorized: false, @@ -727,6 +727,24 @@ const DashboardLayout = () => { return ( <>
+ + + + {/* Header */} {isTracking &&
diff --git a/types.d.ts b/types.d.ts index 8f20363..273cad3 100644 --- a/types.d.ts +++ b/types.d.ts @@ -241,6 +241,9 @@ interface Window { success: boolean; data?: any; }>; + + connectVPN: () => Promise; + disconnectVPN: () => Promise; }; }