Archived
feat: Initial commit with electron-forge
This commit is contained in:
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es6": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:import/recommended",
|
||||||
|
"plugin:import/electron",
|
||||||
|
"plugin:import/typescript"
|
||||||
|
],
|
||||||
|
"parser": "@typescript-eslint/parser"
|
||||||
|
}
|
||||||
+91
-10
@@ -1,13 +1,94 @@
|
|||||||
|
resources/netbird*
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
# Build outputs and dependency folders
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
apps/vynte-connect/.build/
|
lib-cov
|
||||||
apps/vynte-connect/dist/
|
|
||||||
apps/vynte-connect-windows/node_modules/
|
|
||||||
apps/vynte-connect-windows/dist/
|
|
||||||
|
|
||||||
# Locally fetched NetBird runtime assets
|
# Coverage directory used by tools like istanbul
|
||||||
apps/vynte-connect/Resources/netbird
|
coverage
|
||||||
apps/vynte-connect-windows/resources/netbird.exe
|
*.lcov
|
||||||
apps/vynte-connect-windows/resources/wintun.dll
|
|
||||||
.tmp-netbird-assets/
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# TypeScript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
.env.test
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# next.js build output
|
||||||
|
.next
|
||||||
|
|
||||||
|
# nuxt.js build output
|
||||||
|
.nuxt
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# Webpack
|
||||||
|
.webpack/
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
.vite/
|
||||||
|
|
||||||
|
# Electron-Forge
|
||||||
|
out/
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import type { ForgeConfig } from '@electron-forge/shared-types';
|
||||||
|
import { MakerSquirrel } from '@electron-forge/maker-squirrel';
|
||||||
|
import { MakerZIP } from '@electron-forge/maker-zip';
|
||||||
|
import { MakerDeb } from '@electron-forge/maker-deb';
|
||||||
|
import { MakerRpm } from '@electron-forge/maker-rpm';
|
||||||
|
import { VitePlugin } from '@electron-forge/plugin-vite';
|
||||||
|
import { FusesPlugin } from '@electron-forge/plugin-fuses';
|
||||||
|
import { FuseV1Options, FuseVersion } from '@electron/fuses';
|
||||||
|
|
||||||
|
const config: ForgeConfig = {
|
||||||
|
packagerConfig: {
|
||||||
|
asar: true,
|
||||||
|
},
|
||||||
|
rebuildConfig: {},
|
||||||
|
makers: [
|
||||||
|
new MakerSquirrel({}),
|
||||||
|
new MakerZIP({}, ['darwin']),
|
||||||
|
new MakerRpm({}),
|
||||||
|
new MakerDeb({}),
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
new VitePlugin({
|
||||||
|
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
|
||||||
|
// If you are familiar with Vite configuration, it will look really familiar.
|
||||||
|
build: [
|
||||||
|
{
|
||||||
|
// `entry` is just an alias for `build.lib.entry` in the corresponding file of `config`.
|
||||||
|
entry: 'src/main.ts',
|
||||||
|
config: 'vite.main.config.ts',
|
||||||
|
target: 'main',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entry: 'src/preload.ts',
|
||||||
|
config: 'vite.preload.config.ts',
|
||||||
|
target: 'preload',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
renderer: [
|
||||||
|
{
|
||||||
|
name: 'main_window',
|
||||||
|
config: 'vite.renderer.config.ts',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
// Fuses are used to enable/disable various Electron functionality
|
||||||
|
// at package time, before code signing the application
|
||||||
|
new FusesPlugin({
|
||||||
|
version: FuseVersion.V1,
|
||||||
|
[FuseV1Options.RunAsNode]: false,
|
||||||
|
[FuseV1Options.EnableCookieEncryption]: true,
|
||||||
|
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
|
||||||
|
[FuseV1Options.EnableNodeCliInspectArguments]: false,
|
||||||
|
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
|
||||||
|
[FuseV1Options.OnlyLoadAppFromAsar]: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="@electron-forge/plugin-vite/forge-vite-env" />
|
||||||
+70
@@ -0,0 +1,70 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>Vynte Connect</title>
|
||||||
|
<link rel="stylesheet" href="./style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="panel">
|
||||||
|
<header class="header">
|
||||||
|
<img class="logo-small" src="../resources/VynteIcon.png" alt="">
|
||||||
|
<div class="title-block">
|
||||||
|
<div class="title">Vynte Connect</div>
|
||||||
|
<div id="identity" class="identity">Vynte access</div>
|
||||||
|
</div>
|
||||||
|
<button id="refresh" class="icon-button" title="Refresh">↻</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="hero">
|
||||||
|
<button id="orb" class="orb" aria-label="Connect or disconnect">
|
||||||
|
<span class="orb-ring"></span>
|
||||||
|
<span class="orb-core"></span>
|
||||||
|
<span class="orb-dot"></span>
|
||||||
|
<img id="orb-logo" class="orb-logo" src="../resources/VynteIcon.png" alt="">
|
||||||
|
<span id="orb-power" class="orb-power">⏻</span>
|
||||||
|
</button>
|
||||||
|
<div class="status-line">
|
||||||
|
<span id="status-dot" class="status-dot"></span>
|
||||||
|
<span id="status-title">Checking vpn.vyntehome.com</span>
|
||||||
|
</div>
|
||||||
|
<div id="status-message" class="message">Preparing the Vynte access client.</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="gateway">
|
||||||
|
<div class="server-icon">▦</div>
|
||||||
|
<div>
|
||||||
|
<div class="label">Gateway</div>
|
||||||
|
<div class="mono" data-gateway-host>vpn.vyntehome.com</div>
|
||||||
|
</div>
|
||||||
|
<span id="gateway-state" class="badge">Offline</span>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="stats" class="stats hidden">
|
||||||
|
<div class="stat">
|
||||||
|
<div class="label">Internal IP</div>
|
||||||
|
<div id="ip" class="mono">Assigned</div>
|
||||||
|
</div>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="label">Peers</div>
|
||||||
|
<div id="peers" class="mono">0/0</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<button id="auto" class="chip">
|
||||||
|
<span>⚡</span>
|
||||||
|
<span>Auto-connect</span>
|
||||||
|
<span id="toggle" class="toggle"><span></span></span>
|
||||||
|
</button>
|
||||||
|
<span class="spacer"></span>
|
||||||
|
<button id="logout" class="icon-button hidden" title="Log out">◉</button>
|
||||||
|
<button id="quit" class="icon-button" title="Quit">↪</button>
|
||||||
|
</footer>
|
||||||
|
</main>
|
||||||
|
<script src="./renderer.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Generated
+10716
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "vynte-connect",
|
||||||
|
"productName": "vynte-connect",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "My Electron application description",
|
||||||
|
"main": ".vite/build/main.js",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"start": "electron-forge start",
|
||||||
|
"package": "electron-forge package",
|
||||||
|
"make": "electron-forge make",
|
||||||
|
"publish": "electron-forge publish",
|
||||||
|
"lint": "eslint --ext .ts,.tsx ."
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": {
|
||||||
|
"name": "Ishan Karmakar",
|
||||||
|
"email": "[email protected]"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@electron-forge/cli": "^7.11.2",
|
||||||
|
"@electron-forge/maker-deb": "^7.11.2",
|
||||||
|
"@electron-forge/maker-rpm": "^7.11.2",
|
||||||
|
"@electron-forge/maker-squirrel": "^7.11.2",
|
||||||
|
"@electron-forge/maker-zip": "^7.11.2",
|
||||||
|
"@electron-forge/plugin-auto-unpack-natives": "^7.11.2",
|
||||||
|
"@electron-forge/plugin-fuses": "^7.11.2",
|
||||||
|
"@electron-forge/plugin-vite": "^7.11.2",
|
||||||
|
"@electron/fuses": "^1.8.0",
|
||||||
|
"@types/electron-squirrel-startup": "^1.0.2",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||||
|
"@typescript-eslint/parser": "^5.62.0",
|
||||||
|
"electron": "42.3.0",
|
||||||
|
"eslint": "^8.57.1",
|
||||||
|
"eslint-plugin-import": "^2.32.0",
|
||||||
|
"typescript": "~4.5.4",
|
||||||
|
"vite": "^5.4.21"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"electron-squirrel-startup": "^1.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export const MANAGEMENT_URL = 'https://vpn.vyntehome.com';
|
||||||
|
export const GATEWAY_HOST = new URL(MANAGEMENT_URL).host;
|
||||||
|
export const APP_NAME = "Vynte Connect";
|
||||||
|
export const POLL_INTERVAL_MS = 5000;
|
||||||
|
export const DEFAULT_STATUS = {
|
||||||
|
state: "checking",
|
||||||
|
title: `Checking ${GATEWAY_HOST}`,
|
||||||
|
message: "Preparing the Vynte access client.",
|
||||||
|
ip: '',
|
||||||
|
fqdn: '',
|
||||||
|
peers: '0/0',
|
||||||
|
connectedSince: null,
|
||||||
|
gatewayHost: GATEWAY_HOST
|
||||||
|
};
|
||||||
+273
@@ -0,0 +1,273 @@
|
|||||||
|
:root {
|
||||||
|
--bg: #191b20;
|
||||||
|
--panel: rgba(20, 22, 26, .94);
|
||||||
|
--line: rgba(255, 255, 255, .07);
|
||||||
|
--text: #f5f6f8;
|
||||||
|
--muted: #9099a3;
|
||||||
|
--low: #5a5d66;
|
||||||
|
--orange: #ff7a1a;
|
||||||
|
--pink: #e63e5c;
|
||||||
|
--purple: #6f2bae;
|
||||||
|
--blue: #1e90ff;
|
||||||
|
--gradient: linear-gradient(135deg, var(--orange), var(--pink) 38%, var(--purple) 70%, var(--blue));
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html, body { margin: 0; width: 100%; height: 100%; overflow: hidden; }
|
||||||
|
body {
|
||||||
|
color: var(--text);
|
||||||
|
font-family: "Segoe UI Variable", "Segoe UI", system-ui, sans-serif;
|
||||||
|
background:
|
||||||
|
radial-gradient(120% 80% at 20% 0%, rgba(255, 122, 26, .18), transparent 55%),
|
||||||
|
radial-gradient(90% 80% at 100% 100%, rgba(30, 144, 255, .16), transparent 60%),
|
||||||
|
var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
background: rgba(25, 27, 32, .88);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header, .footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
border-bottom: 0;
|
||||||
|
padding: 8px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-small { width: 22px; height: 22px; border-radius: 4px; }
|
||||||
|
.title-block { flex: 1; min-width: 0; }
|
||||||
|
.title { font-size: 13px; font-weight: 650; }
|
||||||
|
.identity {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-family: inherit;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
.icon-button {
|
||||||
|
width: 30px;
|
||||||
|
height: 26px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--muted);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.icon-button:hover { background: rgba(255,255,255,.08); color: var(--text); }
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orb {
|
||||||
|
width: 126px;
|
||||||
|
height: 126px;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
position: relative;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.orb-ring {
|
||||||
|
position: absolute;
|
||||||
|
inset: 5px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: conic-gradient(from 90deg, #2a2d33, #3a3d44, #2a2d33);
|
||||||
|
transition: background .25s, filter .25s;
|
||||||
|
}
|
||||||
|
.orb-core {
|
||||||
|
position: absolute;
|
||||||
|
inset: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(circle at 30% 25%, #1f2228 0%, #14161a 70%);
|
||||||
|
}
|
||||||
|
.orb-logo, .orb-power {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
.orb-logo {
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
display: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.orb-power {
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-size: 42px;
|
||||||
|
color: #7a7e87;
|
||||||
|
}
|
||||||
|
.orb-dot {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
left: 59px;
|
||||||
|
top: 1px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--orange);
|
||||||
|
box-shadow: 0 0 14px rgba(255, 122, 26, .78);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connected .orb-ring,
|
||||||
|
.connecting .orb-ring,
|
||||||
|
.disconnecting .orb-ring,
|
||||||
|
.loggingOut .orb-ring {
|
||||||
|
background: conic-gradient(from 0deg, var(--orange), var(--pink), var(--purple), var(--blue), var(--orange));
|
||||||
|
filter: drop-shadow(0 0 20px rgba(255,122,26,.28));
|
||||||
|
}
|
||||||
|
.connected .orb-logo { display: block; }
|
||||||
|
.connected .orb-power { display: none; }
|
||||||
|
.connecting .orb-ring { animation: spin 1.05s linear infinite, pulse 1s ease-in-out infinite; }
|
||||||
|
.disconnecting .orb-ring, .loggingOut .orb-ring { animation: spin-reverse 1.35s linear infinite, pulse 1s ease-in-out infinite; }
|
||||||
|
.connecting .orb-dot, .disconnecting .orb-dot, .loggingOut .orb-dot { display: block; animation: orbit 1.05s linear infinite; transform-origin: 4px 62px; }
|
||||||
|
.disconnecting .orb-dot, .loggingOut .orb-dot { background: var(--purple); animation-direction: reverse; }
|
||||||
|
|
||||||
|
.status-line {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 650;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.status-dot {
|
||||||
|
display: none;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--orange);
|
||||||
|
box-shadow: 0 0 0 4px rgba(255,122,26,.14), 0 0 14px rgba(255,122,26,.7);
|
||||||
|
}
|
||||||
|
.connected .status-dot, .error .status-dot { display: inline-block; }
|
||||||
|
.error .status-dot { background: var(--pink); box-shadow: none; }
|
||||||
|
.message {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.35;
|
||||||
|
margin-top: 4px;
|
||||||
|
text-align: center;
|
||||||
|
min-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gateway, .stats {
|
||||||
|
margin: 0 14px 12px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: rgba(255,255,255,.04);
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.server-icon {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(255,255,255,.04);
|
||||||
|
color: var(--muted);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
color: var(--low);
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: .6px;
|
||||||
|
}
|
||||||
|
.mono {
|
||||||
|
font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
|
||||||
|
font-size: 12.5px;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
margin-left: auto;
|
||||||
|
padding: 3px 7px;
|
||||||
|
border-radius: 999px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--low);
|
||||||
|
background: rgba(255,255,255,.06);
|
||||||
|
}
|
||||||
|
.connected .badge {
|
||||||
|
color: #ff9747;
|
||||||
|
background: rgba(255,122,26,.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats { padding: 0; gap: 0; overflow: hidden; }
|
||||||
|
.stat {
|
||||||
|
flex: 1;
|
||||||
|
padding: 9px 12px;
|
||||||
|
background: rgba(0,0,0,.16);
|
||||||
|
}
|
||||||
|
.divider { width: 1px; align-self: stretch; background: var(--line); }
|
||||||
|
.hidden { display: none !important; }
|
||||||
|
|
||||||
|
.chip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.chip.active {
|
||||||
|
background: rgba(255,122,26,.10);
|
||||||
|
color: #ff9747;
|
||||||
|
}
|
||||||
|
.toggle {
|
||||||
|
width: 22px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 99px;
|
||||||
|
background: rgba(255,255,255,.10);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.toggle span {
|
||||||
|
position: absolute;
|
||||||
|
top: 1.5px;
|
||||||
|
left: 1.5px;
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: white;
|
||||||
|
transition: left .18s;
|
||||||
|
}
|
||||||
|
.chip.active .toggle {
|
||||||
|
background: var(--gradient);
|
||||||
|
}
|
||||||
|
.chip.active .toggle span { left: 11px; }
|
||||||
|
.spacer { flex: 1; }
|
||||||
|
|
||||||
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
@keyframes spin-reverse { to { transform: rotate(-360deg); } }
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; transform: scale(1); }
|
||||||
|
50% { opacity: .82; transform: scale(1.025); }
|
||||||
|
}
|
||||||
|
@keyframes orbit { to { transform: rotate(360deg); } }
|
||||||
+212
@@ -0,0 +1,212 @@
|
|||||||
|
import { app, BrowserWindow, ipcMain, 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 } from './config';
|
||||||
|
import { NetBirdClient } from './netbird';
|
||||||
|
import { baseStatus, normalizeStatus } from './status';
|
||||||
|
|
||||||
|
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
||||||
|
if (started) {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method will be called when Electron has finished
|
||||||
|
// initialization and is ready to create browser windows.
|
||||||
|
// Some APIs can only be used after this event occurs.
|
||||||
|
app.on('ready', createWindow);
|
||||||
|
|
||||||
|
// 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, 'app', 'resources', name);
|
||||||
|
}
|
||||||
|
return path.join(app.getAppPath(), 'resources', name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function netbirdClient() {
|
||||||
|
return new NetBirdClient(resourcePath('netbird.exe'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function withAutoConnect(status: Record<string, any>) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStatus(status: Record<string, any>) {
|
||||||
|
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.'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return setStatus(normalizeStatus(result.stdout, cachedStatus));
|
||||||
|
}
|
||||||
|
|
||||||
|
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: 384,
|
||||||
|
height: 456,
|
||||||
|
show: false,
|
||||||
|
resizable: false,
|
||||||
|
frame: false,
|
||||||
|
alwaysOnTop: true,
|
||||||
|
skipTaskbar: true,
|
||||||
|
backgroundColor: '#191b20',
|
||||||
|
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 windowBounds = window.getBounds();
|
||||||
|
const x = Math.round(bounds.x + bounds.width - windowBounds.width);
|
||||||
|
const y = Math.round(bounds.y - windowBounds.height - 8);
|
||||||
|
window.setPosition(Math.max(x, 0), Math.max(y, 0), false);
|
||||||
|
window.show();
|
||||||
|
window.focus();
|
||||||
|
broadcastStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTray() {
|
||||||
|
const icon = nativeImage.createFromPath(resourcePath('VynteIcon.png')).resize({ width: 18, height: 18 });
|
||||||
|
tray = new Tray(icon);
|
||||||
|
tray.setToolTip(APP_NAME);
|
||||||
|
tray.on('click', showWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.whenReady().then(() => {
|
||||||
|
createWindow();
|
||||||
|
createTray();
|
||||||
|
refreshStatus();
|
||||||
|
pollTimer = setInterval(refreshStatus, POLL_INTERVAL_MS);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('before-quit', () => {
|
||||||
|
if (pollTimer) clearInterval(pollTimer);
|
||||||
|
});
|
||||||
|
|
||||||
|
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));
|
||||||
+124
@@ -0,0 +1,124 @@
|
|||||||
|
import { execFile } from 'child_process';
|
||||||
|
import { MANAGEMENT_URL } from './config';
|
||||||
|
|
||||||
|
export interface CommandResult {
|
||||||
|
ok: boolean;
|
||||||
|
code?: number;
|
||||||
|
stdout: string;
|
||||||
|
stderr: string;
|
||||||
|
error: Error | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class NetBirdClient {
|
||||||
|
private executablePath: string;
|
||||||
|
|
||||||
|
constructor(executablePath: string) {
|
||||||
|
this.executablePath = executablePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
run(args: string[], options: { timeout?: number } = {}): Promise<CommandResult> {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
execFile(this.executablePath, args, { windowsHide: true, timeout: options.timeout ?? 30000, }, (error, stdout, stderr) => {
|
||||||
|
resolve({
|
||||||
|
ok: !error,
|
||||||
|
code: error && typeof (error as NodeJS.ErrnoException).code === 'number' ? (error as NodeJS.ErrnoException).code : 0,
|
||||||
|
stdout: stdout || '',
|
||||||
|
stderr: stderr || '',
|
||||||
|
error
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
runElevated(args: string[]): Promise<Omit<CommandResult, 'code'>> {
|
||||||
|
const quotedExe = this.executablePath.replace(/'/g, "''");
|
||||||
|
const quotedArgs = args.map(arg => `'${String(arg).replace(/'/g, "''")}'`).join(',');
|
||||||
|
|
||||||
|
const script = `Start-Process -FilePath '${quotedExe}' -ArgumentList @(${quotedArgs}) -Verb RunAs -Wait`;
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
execFile(
|
||||||
|
'powershell.exe',
|
||||||
|
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', script],
|
||||||
|
{
|
||||||
|
windowsHide: true,
|
||||||
|
timeout: 180000,
|
||||||
|
},
|
||||||
|
(error, stdout, stderr) => {
|
||||||
|
resolve({
|
||||||
|
ok: !error,
|
||||||
|
stdout: stdout || '',
|
||||||
|
stderr: stderr || '',
|
||||||
|
error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
status(options: { timeout?: number } = {}): Promise<CommandResult> {
|
||||||
|
return this.run(['status', '--json'], {
|
||||||
|
timeout: options.timeout ?? 12000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async ensureService(): Promise<boolean> {
|
||||||
|
const status = await this.status({ timeout: 10000 });
|
||||||
|
|
||||||
|
if (status.ok) return true;
|
||||||
|
|
||||||
|
await this.runElevated([
|
||||||
|
'service',
|
||||||
|
'install',
|
||||||
|
'--management-url',
|
||||||
|
MANAGEMENT_URL,
|
||||||
|
'--admin-url',
|
||||||
|
MANAGEMENT_URL,
|
||||||
|
'--log-level',
|
||||||
|
'info'
|
||||||
|
]);
|
||||||
|
|
||||||
|
await this.runElevated(['service', 'start']);
|
||||||
|
|
||||||
|
for (let index = 0; index < 20; index++) {
|
||||||
|
const check = await this.status({ timeout: 8000 });
|
||||||
|
if (check.ok) return true;
|
||||||
|
|
||||||
|
await new Promise<void>(resolve => setTimeout(resolve, 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(): Promise<CommandResult> {
|
||||||
|
return this.run([
|
||||||
|
'up',
|
||||||
|
'--management-url',
|
||||||
|
MANAGEMENT_URL,
|
||||||
|
'--admin-url',
|
||||||
|
MANAGEMENT_URL
|
||||||
|
], { timeout: 180000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect(): Promise<CommandResult> {
|
||||||
|
return this.run([
|
||||||
|
'down',
|
||||||
|
'--management-url', MANAGEMENT_URL
|
||||||
|
], { timeout: 45000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
async logout(): Promise<CommandResult> {
|
||||||
|
await this.run(
|
||||||
|
['down', '--management-url', MANAGEMENT_URL],
|
||||||
|
{ timeout: 30000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.run([
|
||||||
|
'deregister',
|
||||||
|
'--management-url',
|
||||||
|
MANAGEMENT_URL,
|
||||||
|
'--admin-url',
|
||||||
|
MANAGEMENT_URL
|
||||||
|
], { timeout: 60000 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// See the Electron documentation for details on how to use preload scripts:
|
||||||
|
// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
|
||||||
|
|
||||||
|
import { contextBridge, ipcRenderer } from "electron";
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('vynte', {
|
||||||
|
getStatus: () => ipcRenderer.invoke('status:get'),
|
||||||
|
refresh: () => ipcRenderer.invoke('status:refresh'),
|
||||||
|
connect: () => ipcRenderer.invoke('vpn:connect'),
|
||||||
|
disconnect: () => ipcRenderer.invoke('vpn:disconnect'),
|
||||||
|
logout: () => ipcRenderer.invoke('vpn:logout'),
|
||||||
|
toggleAuto: () => ipcRenderer.invoke('auto:toggle'),
|
||||||
|
quit: () => ipcRenderer.invoke('app:quit'),
|
||||||
|
openGateway: () => ipcRenderer.invoke('external:openGateway'),
|
||||||
|
onStatus: (callback: Function) => ipcRenderer.on('status', (_, status) => callback(status))
|
||||||
|
})
|
||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* This file will automatically be loaded by vite and run in the "renderer" context.
|
||||||
|
* To learn more about the differences between the "main" and the "renderer" context in
|
||||||
|
* Electron, visit:
|
||||||
|
*
|
||||||
|
* https://electronjs.org/docs/tutorial/process-model
|
||||||
|
*
|
||||||
|
* By default, Node.js integration in this file is disabled. When enabling Node.js integration
|
||||||
|
* in a renderer process, please be aware of potential security implications. You can read
|
||||||
|
* more about security risks here:
|
||||||
|
*
|
||||||
|
* https://electronjs.org/docs/tutorial/security
|
||||||
|
*
|
||||||
|
* To enable Node.js integration in this file, open up `main.ts` and enable the `nodeIntegration`
|
||||||
|
* flag:
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* // Create the browser window.
|
||||||
|
* mainWindow = new BrowserWindow({
|
||||||
|
* width: 800,
|
||||||
|
* height: 600,
|
||||||
|
* webPreferences: {
|
||||||
|
* nodeIntegration: true
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { NetworkStatus } from './status';
|
||||||
|
|
||||||
|
const body = document.body;
|
||||||
|
|
||||||
|
const orb = document.getElementById('orb') as HTMLElement;
|
||||||
|
const statusTitle = document.getElementById('status-title') as HTMLElement;
|
||||||
|
const statusMessage = document.getElementById('status-message') as HTMLElement;
|
||||||
|
const identity = document.getElementById('identity') as HTMLElement;
|
||||||
|
const gatewayState = document.getElementById('gateway-state') as HTMLElement;
|
||||||
|
const stats = document.getElementById('stats') as HTMLElement;
|
||||||
|
const ip = document.getElementById('ip') as HTMLElement;
|
||||||
|
const peers = document.getElementById('peers') as HTMLElement;
|
||||||
|
const refresh = document.getElementById('refresh') as HTMLElement;
|
||||||
|
const auto = document.getElementById('auto') as HTMLElement;
|
||||||
|
const logout = document.getElementById('logout') as HTMLElement;
|
||||||
|
const quit = document.getElementById('quit') as HTMLElement;
|
||||||
|
|
||||||
|
interface UIStatus extends NetworkStatus {
|
||||||
|
autoConnect?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentStatus: Partial<UIStatus> = {
|
||||||
|
state: 'checking',
|
||||||
|
};
|
||||||
|
|
||||||
|
function setStatus(status: UIStatus): void {
|
||||||
|
currentStatus = status;
|
||||||
|
|
||||||
|
body.className = status.state ?? 'disconnected';
|
||||||
|
statusTitle.textContent = status.title ?? 'Disconnected';
|
||||||
|
statusMessage.textContent = status.message ?? '';
|
||||||
|
identity.textContent = status.fqdn ?? 'Vynte access';
|
||||||
|
|
||||||
|
const gatewayHostElement = document.querySelector(
|
||||||
|
'[data-gateway-host]'
|
||||||
|
) as HTMLElement | null;
|
||||||
|
|
||||||
|
if (gatewayHostElement) {
|
||||||
|
gatewayHostElement.textContent =
|
||||||
|
status.gatewayHost ?? 'vpn.vyntehome.com';
|
||||||
|
}
|
||||||
|
|
||||||
|
gatewayState.textContent =
|
||||||
|
status.state === 'connected' ? 'Live' : 'Offline';
|
||||||
|
|
||||||
|
ip.textContent = status.ip ?? 'Assigned';
|
||||||
|
peers.textContent = status.peers ?? '0/0';
|
||||||
|
|
||||||
|
stats.classList.toggle('hidden', status.state !== 'connected');
|
||||||
|
logout.classList.toggle('hidden', status.state !== 'connected');
|
||||||
|
auto.classList.toggle('active', Boolean(status.autoConnect));
|
||||||
|
}
|
||||||
|
|
||||||
|
orb.addEventListener('click', async () => {
|
||||||
|
if (currentStatus.state === 'connected') {
|
||||||
|
await window.vynte.disconnect();
|
||||||
|
} else if (
|
||||||
|
![
|
||||||
|
'connecting',
|
||||||
|
'disconnecting',
|
||||||
|
'loggingOut',
|
||||||
|
'checking',
|
||||||
|
].includes(currentStatus.state ?? '')
|
||||||
|
) {
|
||||||
|
await window.vynte.connect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
refresh.addEventListener('click', () => {
|
||||||
|
void window.vynte.refresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
auto.addEventListener('click', () => {
|
||||||
|
void window.vynte.toggleAuto();
|
||||||
|
});
|
||||||
|
|
||||||
|
logout.addEventListener('click', () => {
|
||||||
|
void window.vynte.logout();
|
||||||
|
});
|
||||||
|
|
||||||
|
quit.addEventListener('click', () => {
|
||||||
|
void window.vynte.quit();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.vynte.onStatus(setStatus);
|
||||||
|
window.vynte.getStatus().then(setStatus);
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { GATEWAY_HOST } from './config';
|
||||||
|
|
||||||
|
export interface NetworkStatus {
|
||||||
|
state?: string;
|
||||||
|
title?: string;
|
||||||
|
message?: string;
|
||||||
|
ip: string;
|
||||||
|
fqdn: string;
|
||||||
|
peers: string;
|
||||||
|
connectedSince: number | null;
|
||||||
|
gatewayHost: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NetBirdStatusResponse {
|
||||||
|
daemonStatus?: string;
|
||||||
|
netbirdIp?: string;
|
||||||
|
fqdn?: string;
|
||||||
|
management?: {
|
||||||
|
connected?: boolean;
|
||||||
|
};
|
||||||
|
signal?: {
|
||||||
|
connected?: boolean;
|
||||||
|
};
|
||||||
|
peers?: {
|
||||||
|
connected?: number;
|
||||||
|
total?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function baseStatus(
|
||||||
|
overrides: Partial<NetworkStatus> = {}
|
||||||
|
): NetworkStatus {
|
||||||
|
return {
|
||||||
|
ip: '',
|
||||||
|
fqdn: '',
|
||||||
|
peers: '0/0',
|
||||||
|
connectedSince: null,
|
||||||
|
gatewayHost: GATEWAY_HOST,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeStatus(
|
||||||
|
raw: string,
|
||||||
|
previousStatus: Partial<NetworkStatus> = {}
|
||||||
|
): NetworkStatus {
|
||||||
|
let parsed: NetBirdStatusResponse;
|
||||||
|
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(raw) as NetBirdStatusResponse;
|
||||||
|
} catch {
|
||||||
|
return baseStatus({
|
||||||
|
state: 'error',
|
||||||
|
title: 'Connection failed',
|
||||||
|
message: `${GATEWAY_HOST} status could not be parsed.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const daemonConnected = String(parsed.daemonStatus ?? '')
|
||||||
|
.toLowerCase()
|
||||||
|
.includes('connected');
|
||||||
|
|
||||||
|
const managementConnected = parsed.management?.connected === true;
|
||||||
|
const signalConnected = parsed.signal?.connected === true;
|
||||||
|
const hasIdentity = Boolean(parsed.netbirdIp || parsed.fqdn);
|
||||||
|
|
||||||
|
const connected =
|
||||||
|
daemonConnected &&
|
||||||
|
(managementConnected || signalConnected || hasIdentity);
|
||||||
|
|
||||||
|
const peerCount = parsed.peers ?? {};
|
||||||
|
|
||||||
|
if (connected) {
|
||||||
|
return baseStatus({
|
||||||
|
state: 'connected',
|
||||||
|
title: 'Connected',
|
||||||
|
message: "You're on the Vynte network.",
|
||||||
|
ip: parsed.netbirdIp || 'Assigned',
|
||||||
|
fqdn: parsed.fqdn || '',
|
||||||
|
peers: `${peerCount.connected ?? 0}/${peerCount.total ?? 0}`,
|
||||||
|
connectedSince:
|
||||||
|
previousStatus.connectedSince ?? Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseStatus({
|
||||||
|
state: 'disconnected',
|
||||||
|
title: 'Disconnected',
|
||||||
|
message: 'Click the flame to connect with NetBird.',
|
||||||
|
fqdn: parsed.fqdn || '',
|
||||||
|
peers: `${peerCount.connected ?? 0}/${peerCount.total ?? 0}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "commonjs",
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"outDir": "dist",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config
|
||||||
|
export default defineConfig({});
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config
|
||||||
|
export default defineConfig({});
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config
|
||||||
|
export default defineConfig({});
|
||||||
Reference in New Issue
Block a user