This repository has been archived on 2026-07-05. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
vynte-connect/src/main.ts
T

246 lines
6.5 KiB
TypeScript

import { app, BrowserWindow, ipcMain, Menu, MenuItemConstructorOptions, nativeImage, shell, Tray } from 'electron';
import path from 'node:path';
import started from 'electron-squirrel-startup';
import { APP_NAME, DEFAULT_STATUS, GATEWAY_HOST, MANAGEMENT_URL, POLL_INTERVAL_MS, WINDOW_HEIGHT, WINDOW_WIDTH } from './config';
import { NetBirdClient } from './netbird';
import { baseStatus, normalizeStatus } from './status';
import { NetworkStatus, UIStatus } from './types';
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (started) {
app.quit();
}
const singleInstanceLock = app.requestSingleInstanceLock()
if (!singleInstanceLock) app.quit();
else {
app.on('second-instance', () => {
if (window) {
if (window.isMinimized()) window.restore();
window.focus();
}
});
app.on('before-quit', disconnect);
}
app.whenReady().then(() => {
createWindow();
createTray();
refreshStatus();
pollTimer = setInterval(refreshStatus, POLL_INTERVAL_MS);
});
app.on('before-quit', () => {
if (pollTimer) clearInterval(pollTimer);
});
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and import them here.
let tray: Tray;
let window: BrowserWindow;
let pollTimer: NodeJS.Timeout;
let autoConnect = false;
let cachedStatus = DEFAULT_STATUS;
function resourcePath(name: string) {
if (app.isPackaged) {
return path.join(process.resourcesPath, 'resources', name);
}
return path.join(app.getAppPath(), 'resources', name);
}
function netbirdClient() {
return new NetBirdClient(resourcePath(process.platform === "win32" ? 'netbird.exe' : 'netbird'));
}
function withAutoConnect(status: NetworkStatus): UIStatus {
return { ...status, autoConnect };
}
function broadcastStatus() {
if (window && !window.isDestroyed()) {
window.webContents.send('status', withAutoConnect(cachedStatus));
}
}
function updateTray() {
if (!tray) return;
const label = cachedStatus.state === 'connected' ? `${APP_NAME}: Connected` : APP_NAME;
tray.setToolTip(label);
const template: MenuItemConstructorOptions[] = [];
const connected = cachedStatus.state === "connected";
const busy = ['connecting', 'disconnecting'].includes(cachedStatus.state);
template.push({
label: connected ? 'Disconnect' : 'Connect',
click: connected ? disconnect : connect,
enabled: !busy
});
template.push(
{
label: 'Logout',
click: logout
},
{ role: 'quit' }
);
tray.setContextMenu(Menu.buildFromTemplate(template));
}
function setStatus(status: NetworkStatus) {
cachedStatus = {
...cachedStatus,
...status,
gatewayHost: GATEWAY_HOST
};
broadcastStatus();
updateTray();
return cachedStatus;
}
async function refreshStatus() {
const result = await netbirdClient().status();
if (!result.ok) {
return setStatus(baseStatus({
state: 'disconnected',
title: 'Disconnected',
message: 'Click the flame to install the Vynte network helper.'
}));
}
const status = normalizeStatus(result.stdout, cachedStatus);
if (status.state === "disconnected" && cachedStatus.state === "connecting") return;
setStatus(status);
}
async function connect() {
setStatus({
state: 'connecting',
title: 'Connecting...',
message: 'Starting secure Vynte access...'
});
const client = netbirdClient();
const serviceReady = await client.ensureService();
if (!serviceReady) {
return setStatus({
state: 'error',
title: 'Connection failed',
message: `${GATEWAY_HOST} helper did not become ready.`
});
}
const result = await client.connect();
if (!result.ok) {
const output = `${result.stdout}\n${result.stderr}`.trim();
return setStatus({
state: 'error',
title: 'Connection failed',
message: output || `${GATEWAY_HOST} command failed.`
});
}
return refreshStatus();
}
async function disconnect() {
setStatus({
state: 'disconnecting',
title: 'Disconnecting...',
message: 'Closing secure Vynte access...'
});
await netbirdClient().disconnect();
return refreshStatus();
}
async function logout() {
setStatus({
state: 'loggingOut',
title: 'Logging out',
message: "Removing this PC's NetBird registration."
});
await netbirdClient().logout();
return refreshStatus();
}
function createWindow() {
window = new BrowserWindow({
width: WINDOW_WIDTH,
height: WINDOW_HEIGHT,
show: false,
resizable: false,
frame: false,
alwaysOnTop: true,
skipTaskbar: true,
backgroundColor: '#191b20',
icon: resourcePath("VynteIcon.png"),
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true
}
});
// and load the index.html of the app.
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
window.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
} else {
window.loadFile(
path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`),
);
}
window.on('blur', () => window.hide());
}
function showWindow() {
const bounds = tray.getBounds();
const x = Math.round(bounds.x - WINDOW_WIDTH);
const y = Math.round(bounds.y - WINDOW_HEIGHT);
window.setBounds({
width: WINDOW_WIDTH,
height: WINDOW_HEIGHT,
x, y
});
window.show();
window.focus();
broadcastStatus();
}
function createTray() {
const icon = nativeImage.createFromPath(resourcePath('VynteIcon.png')).resize({ width: 18, height: 18 });
tray = new Tray(icon);
updateTray();
tray.on('click', showWindow);
}
ipcMain.handle('status:get', () => withAutoConnect(cachedStatus));
ipcMain.handle('status:refresh', refreshStatus);
ipcMain.handle('vpn:connect', connect);
ipcMain.handle('vpn:disconnect', disconnect);
ipcMain.handle('vpn:logout', logout);
ipcMain.handle('auto:toggle', () => {
autoConnect = !autoConnect;
broadcastStatus();
return autoConnect;
});
ipcMain.handle('app:quit', () => app.quit());
ipcMain.handle('external:openGateway', () => shell.openExternal(MANAGEMENT_URL));