mirror of
https://github.com/EasyTier/EasyTier.git
synced 2024-11-16 11:42:27 +08:00
Compare commits
No commits in common. "7dcc0ae70e1221d8e890a3721e0293fb7a07902e" and "780bb6dcdbee8f0205fd4439d09bcddcdc822951" have entirely different histories.
7dcc0ae70e
...
780bb6dcdb
766
Cargo.lock
generated
766
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
2
easytier-gui/.npmrc
Normal file
2
easytier-gui/.npmrc
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
shamefully-hoist=true
|
||||||
|
strict-peer-dependencies=false
|
|
@ -13,32 +13,34 @@
|
||||||
"lint:fix": "eslint . --ignore-pattern src-tauri --fix"
|
"lint:fix": "eslint . --ignore-pattern src-tauri --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@primevue/themes": "^4.2.1",
|
"@primevue/themes": "^4.1.0",
|
||||||
"@tauri-apps/plugin-autostart": "2.0.0",
|
"@tauri-apps/plugin-autostart": "2.0.0-rc.1",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "2.0.0",
|
"@tauri-apps/plugin-clipboard-manager": "2.0.0-rc.1",
|
||||||
"@tauri-apps/plugin-os": "2.0.0",
|
"@tauri-apps/plugin-os": "2.0.0-rc.1",
|
||||||
"@tauri-apps/plugin-process": "2.0.0",
|
"@tauri-apps/plugin-process": "2.0.0-rc.1",
|
||||||
"@tauri-apps/plugin-shell": "2.0.1",
|
"@tauri-apps/plugin-shell": "2.0.0-rc.1",
|
||||||
"@vueuse/core": "^11.2.0",
|
"@vueuse/core": "^11.1.0",
|
||||||
"aura": "link:@primevue\\themes\\aura",
|
"aura": "link:@primevue\\themes\\aura",
|
||||||
"easytier-frontend-lib": "workspace:*",
|
|
||||||
"ip-num": "1.5.1",
|
"ip-num": "1.5.1",
|
||||||
"pinia": "^2.2.4",
|
"pinia": "^2.2.4",
|
||||||
"primevue": "^4.2.1",
|
"primeflex": "^3.3.1",
|
||||||
"tauri-plugin-vpnservice-api": "workspace:*",
|
"primeicons": "^7.0.0",
|
||||||
"vue": "^3.5.12",
|
"primevue": "^4.1.0",
|
||||||
|
"tauri-plugin-vpnservice-api": "link:..\\tauri-plugin-vpnservice",
|
||||||
|
"vue": "=3.4.38",
|
||||||
|
"vue-i18n": "^10.0.4",
|
||||||
"vue-router": "^4.4.5"
|
"vue-router": "^4.4.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@antfu/eslint-config": "^3.7.3",
|
"@antfu/eslint-config": "^3.7.3",
|
||||||
"@intlify/unplugin-vue-i18n": "^5.2.0",
|
"@intlify/unplugin-vue-i18n": "^5.2.0",
|
||||||
"@primevue/auto-import-resolver": "^4.1.0",
|
"@primevue/auto-import-resolver": "^4.1.0",
|
||||||
"@tauri-apps/api": "2.1.0",
|
"@tauri-apps/api": "2.0.0-rc.0",
|
||||||
"@tauri-apps/cli": "2.1.0",
|
"@tauri-apps/cli": "2.0.0-rc.3",
|
||||||
"@types/node": "^22.7.4",
|
"@types/node": "^22.7.4",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@vitejs/plugin-vue": "^5.1.4",
|
"@vitejs/plugin-vue": "^5.1.4",
|
||||||
"@vue-macros/volar": "0.30.5",
|
"@vue-macros/volar": "0.30.3",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.12.0",
|
||||||
"eslint-plugin-format": "^0.1.2",
|
"eslint-plugin-format": "^0.1.2",
|
||||||
|
@ -48,7 +50,7 @@
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.6.2",
|
||||||
"unplugin-auto-import": "^0.18.3",
|
"unplugin-auto-import": "^0.18.3",
|
||||||
"unplugin-vue-components": "^0.27.4",
|
"unplugin-vue-components": "^0.27.4",
|
||||||
"unplugin-vue-macros": "^2.13.3",
|
"unplugin-vue-macros": "^2.12.3",
|
||||||
"unplugin-vue-markdown": "^0.26.2",
|
"unplugin-vue-markdown": "^0.26.2",
|
||||||
"unplugin-vue-router": "^0.10.8",
|
"unplugin-vue-router": "^0.10.8",
|
||||||
"uuid": "^10.0.0",
|
"uuid": "^10.0.0",
|
||||||
|
@ -56,6 +58,6 @@
|
||||||
"vite-plugin-vue-devtools": "^7.4.6",
|
"vite-plugin-vue-devtools": "^7.4.6",
|
||||||
"vite-plugin-vue-layouts": "^0.11.0",
|
"vite-plugin-vue-layouts": "^0.11.0",
|
||||||
"vue-i18n": "^10.0.0",
|
"vue-i18n": "^10.0.0",
|
||||||
"vue-tsc": "^2.1.10"
|
"vue-tsc": "^2.1.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,11 +15,10 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
tauri-build = { version = "2.0.0-rc", features = [] }
|
tauri-build = { version = "2.0.0-rc", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "2.1", features = [
|
tauri = { version = "2.0.0-rc", features = [
|
||||||
"tray-icon",
|
"tray-icon",
|
||||||
"image-png",
|
"image-png",
|
||||||
"image-ico",
|
"image-ico",
|
||||||
"devtools",
|
|
||||||
] }
|
] }
|
||||||
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
@ -38,13 +37,13 @@ gethostname = "0.5"
|
||||||
|
|
||||||
dunce = "1.0.4"
|
dunce = "1.0.4"
|
||||||
|
|
||||||
tauri-plugin-shell = "2.0"
|
tauri-plugin-shell = "2.0.0-rc"
|
||||||
tauri-plugin-process = "2.0"
|
tauri-plugin-process = "2.0.0-rc"
|
||||||
tauri-plugin-clipboard-manager = "2.0"
|
tauri-plugin-clipboard-manager = "2.0.0-rc"
|
||||||
tauri-plugin-positioner = { version = "2.0", features = ["tray-icon"] }
|
tauri-plugin-positioner = { version = "2.0.0-rc", features = ["tray-icon"] }
|
||||||
tauri-plugin-vpnservice = { path = "../../tauri-plugin-vpnservice" }
|
tauri-plugin-vpnservice = { path = "../../tauri-plugin-vpnservice" }
|
||||||
tauri-plugin-os = "2.0"
|
tauri-plugin-os = "2.0.0-rc"
|
||||||
tauri-plugin-autostart = "2.0"
|
tauri-plugin-autostart = "2.0.0-rc"
|
||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|
|
@ -3,12 +3,17 @@
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use easytier::{
|
use easytier::{
|
||||||
common::config::{ConfigLoader, FileLoggerConfig, TomlConfigLoader},
|
common::config::{
|
||||||
|
ConfigLoader, FileLoggerConfig, Flags, NetworkIdentity, PeerConfig, TomlConfigLoader,
|
||||||
|
VpnPortalConfig,
|
||||||
|
},
|
||||||
launcher::{NetworkConfig, NetworkInstance, NetworkInstanceRunningInfo},
|
launcher::{NetworkConfig, NetworkInstance, NetworkInstanceRunningInfo},
|
||||||
utils::{self, NewFilterSender},
|
utils::{self, NewFilterSender},
|
||||||
};
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use tauri::Manager as _;
|
use tauri::Manager as _;
|
||||||
|
|
||||||
|
|
2
easytier-gui/src/auto-imports.d.ts
vendored
2
easytier-gui/src/auto-imports.d.ts
vendored
|
@ -154,6 +154,8 @@ declare module 'vue' {
|
||||||
readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
|
readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
|
||||||
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
||||||
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
||||||
|
readonly num2ipv4: UnwrapRef<typeof import('./composables/utils')['num2ipv4']>
|
||||||
|
readonly num2ipv6: UnwrapRef<typeof import('./composables/utils')['num2ipv6']>
|
||||||
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
|
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
|
||||||
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
|
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
|
||||||
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
|
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
|
||||||
|
|
296
easytier-gui/src/components/Config.vue
Normal file
296
easytier-gui/src/components/Config.vue
Normal file
|
@ -0,0 +1,296 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import InputGroup from 'primevue/inputgroup'
|
||||||
|
import InputGroupAddon from 'primevue/inputgroupaddon'
|
||||||
|
import { getOsHostname } from '~/composables/network'
|
||||||
|
|
||||||
|
import { NetworkingMethod } from '~/types/network'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
configInvalid?: boolean
|
||||||
|
instanceId?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineEmits(['runNetwork'])
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const networking_methods = ref([
|
||||||
|
{ value: NetworkingMethod.PublicServer, label: () => t('public_server') },
|
||||||
|
{ value: NetworkingMethod.Manual, label: () => t('manual') },
|
||||||
|
{ value: NetworkingMethod.Standalone, label: () => t('standalone') },
|
||||||
|
])
|
||||||
|
|
||||||
|
const networkStore = useNetworkStore()
|
||||||
|
const curNetwork = computed(() => {
|
||||||
|
if (props.instanceId) {
|
||||||
|
// console.log('instanceId', props.instanceId)
|
||||||
|
const c = networkStore.networkList.find(n => n.instance_id === props.instanceId)
|
||||||
|
if (c !== undefined)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
return networkStore.curNetwork
|
||||||
|
})
|
||||||
|
|
||||||
|
const protos: { [proto: string]: number } = { tcp: 11010, udp: 11010, wg: 11011, ws: 11011, wss: 11012 }
|
||||||
|
|
||||||
|
function searchUrlSuggestions(e: { query: string }): string[] {
|
||||||
|
const query = e.query
|
||||||
|
const ret = []
|
||||||
|
// if query match "^\w+:.*", then no proto prefix
|
||||||
|
if (query.match(/^\w+:.*/)) {
|
||||||
|
// if query is a valid url, then add to suggestions
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new URL(query)
|
||||||
|
ret.push(query)
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
for (const proto in protos) {
|
||||||
|
let item = `${proto}://${query}`
|
||||||
|
// if query match ":\d+$", then no port suffix
|
||||||
|
if (!query.match(/:\d+$/)) {
|
||||||
|
item += `:${protos[proto]}`
|
||||||
|
}
|
||||||
|
ret.push(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
const publicServerSuggestions = ref([''])
|
||||||
|
|
||||||
|
function searchPresetPublicServers(e: { query: string }) {
|
||||||
|
const presetPublicServers = [
|
||||||
|
'tcp://public.easytier.top:11010',
|
||||||
|
]
|
||||||
|
|
||||||
|
const query = e.query
|
||||||
|
// if query is sub string of presetPublicServers, add to suggestions
|
||||||
|
let ret = presetPublicServers.filter(item => item.includes(query))
|
||||||
|
// add additional suggestions
|
||||||
|
if (query.length > 0) {
|
||||||
|
ret = ret.concat(searchUrlSuggestions(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
publicServerSuggestions.value = ret
|
||||||
|
}
|
||||||
|
|
||||||
|
const peerSuggestions = ref([''])
|
||||||
|
|
||||||
|
function searchPeerSuggestions(e: { query: string }) {
|
||||||
|
peerSuggestions.value = searchUrlSuggestions(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
const inetSuggestions = ref([''])
|
||||||
|
|
||||||
|
function searchInetSuggestions(e: { query: string }) {
|
||||||
|
if (e.query.search('/') >= 0) {
|
||||||
|
inetSuggestions.value = [e.query]
|
||||||
|
} else {
|
||||||
|
const ret = []
|
||||||
|
for (let i = 0; i < 32; i++) {
|
||||||
|
ret.push(`${e.query}/${i}`)
|
||||||
|
}
|
||||||
|
inetSuggestions.value = ret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const listenerSuggestions = ref([''])
|
||||||
|
|
||||||
|
function searchListenerSuggestiong(e: { query: string }) {
|
||||||
|
const ret = []
|
||||||
|
|
||||||
|
for (const proto in protos) {
|
||||||
|
let item = `${proto}://0.0.0.0:`
|
||||||
|
// if query is a number, use it as port
|
||||||
|
if (e.query.match(/^\d+$/)) {
|
||||||
|
item += e.query
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
item += protos[proto]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.includes(e.query)) {
|
||||||
|
ret.push(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ret.length === 0) {
|
||||||
|
ret.push(e.query)
|
||||||
|
}
|
||||||
|
|
||||||
|
listenerSuggestions.value = ret
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateHostname() {
|
||||||
|
if (curNetwork.value.hostname) {
|
||||||
|
// eslint no-useless-escape
|
||||||
|
let name = curNetwork.value.hostname!.replaceAll(/[^\u4E00-\u9FA5a-z0-9\-]*/gi, '')
|
||||||
|
if (name.length > 32)
|
||||||
|
name = name.substring(0, 32)
|
||||||
|
|
||||||
|
if (curNetwork.value.hostname !== name)
|
||||||
|
curNetwork.value.hostname = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const osHostname = ref<string>('')
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
osHostname.value = await getOsHostname()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-column h-full">
|
||||||
|
<div class="flex flex-column">
|
||||||
|
<div class="w-10/12 self-center ">
|
||||||
|
<Panel :header="t('basic_settings')">
|
||||||
|
<div class="flex flex-column gap-y-2">
|
||||||
|
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||||
|
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||||
|
<div class="flex align-items-center" for="virtual_ip">
|
||||||
|
<label class="mr-2"> {{ t('virtual_ipv4') }} </label>
|
||||||
|
<Checkbox v-model="curNetwork.dhcp" input-id="virtual_ip_auto" :binary="true" />
|
||||||
|
|
||||||
|
<label for="virtual_ip_auto" class="ml-2">
|
||||||
|
{{ t('virtual_ipv4_dhcp') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<InputGroup>
|
||||||
|
<InputText id="virtual_ip" v-model="curNetwork.virtual_ipv4" :disabled="curNetwork.dhcp"
|
||||||
|
aria-describedby="virtual_ipv4-help" />
|
||||||
|
<InputGroupAddon>
|
||||||
|
<span>/</span>
|
||||||
|
</InputGroupAddon>
|
||||||
|
<InputNumber v-model="curNetwork.network_length" :disabled="curNetwork.dhcp"
|
||||||
|
inputId="horizontal-buttons" showButtons :step="1" mode="decimal" :min="1" :max="32" fluid
|
||||||
|
class="max-w-20" />
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||||
|
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||||
|
<label for="network_name">{{ t('network_name') }}</label>
|
||||||
|
<InputText id="network_name" v-model="curNetwork.network_name" aria-describedby="network_name-help" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||||
|
<label for="network_secret">{{ t('network_secret') }}</label>
|
||||||
|
<InputText id="network_secret" v-model="curNetwork.network_secret"
|
||||||
|
aria-describedby="network_secret-help" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||||
|
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||||
|
<label for="nm">{{ t('networking_method') }}</label>
|
||||||
|
<SelectButton v-model="curNetwork.networking_method" :options="networking_methods"
|
||||||
|
:option-label="(v: any) => v.label()" option-value="value" />
|
||||||
|
<div class="items-center flex flex-row p-fluid gap-x-1">
|
||||||
|
<AutoComplete v-if="curNetwork.networking_method === NetworkingMethod.Manual" id="chips"
|
||||||
|
v-model="curNetwork.peer_urls" :placeholder="t('chips_placeholder', ['tcp://8.8.8.8:11010'])"
|
||||||
|
class="grow" multiple fluid :suggestions="peerSuggestions" @complete="searchPeerSuggestions" />
|
||||||
|
|
||||||
|
<AutoComplete v-if="curNetwork.networking_method === NetworkingMethod.PublicServer"
|
||||||
|
v-model="curNetwork.public_server_url" :suggestions="publicServerSuggestions"
|
||||||
|
:virtual-scroller-options="{ itemSize: 38 }" class="grow" dropdown :complete-on-focus="true"
|
||||||
|
@complete="searchPresetPublicServers" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Panel :header="t('advanced_settings')" toggleable collapsed>
|
||||||
|
<div class="flex flex-column gap-y-2">
|
||||||
|
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||||
|
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||||
|
<div class="flex align-items-center">
|
||||||
|
<Checkbox v-model="curNetwork.latency_first" input-id="use_latency_first" :binary="true" />
|
||||||
|
<label for="use_latency_first" class="ml-2"> {{ t('use_latency_first') }} </label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||||
|
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||||
|
<label for="hostname">{{ t('hostname') }}</label>
|
||||||
|
<InputText id="hostname" v-model="curNetwork.hostname" aria-describedby="hostname-help" :format="true"
|
||||||
|
:placeholder="t('hostname_placeholder', [osHostname])" @blur="validateHostname" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-x-9 flex-wrap w-full">
|
||||||
|
<div class="flex flex-column gap-2 grow p-fluid">
|
||||||
|
<label for="username">{{ t('proxy_cidrs') }}</label>
|
||||||
|
<AutoComplete id="subnet-proxy" v-model="curNetwork.proxy_cidrs"
|
||||||
|
:placeholder="t('chips_placeholder', ['10.0.0.0/24'])" class="w-full" multiple fluid
|
||||||
|
:suggestions="inetSuggestions" @complete="searchInetSuggestions" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-x-9 flex-wrap ">
|
||||||
|
<div class="flex flex-column gap-2 grow">
|
||||||
|
<label for="username">VPN Portal</label>
|
||||||
|
<ToggleButton v-model="curNetwork.enable_vpn_portal" on-icon="pi pi-check" off-icon="pi pi-times"
|
||||||
|
:on-label="t('off_text')" :off-label="t('on_text')" class="w-48" />
|
||||||
|
<div v-if="curNetwork.enable_vpn_portal" class="items-center flex flex-row gap-x-4">
|
||||||
|
<div class="min-w-64">
|
||||||
|
<InputGroup>
|
||||||
|
<InputText v-model="curNetwork.vpn_portal_client_network_addr"
|
||||||
|
:placeholder="t('vpn_portal_client_network')" />
|
||||||
|
<InputGroupAddon>
|
||||||
|
<span>/{{ curNetwork.vpn_portal_client_network_len }}</span>
|
||||||
|
</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
|
||||||
|
<InputNumber v-model="curNetwork.vpn_portal_listen_port" :allow-empty="false" :format="false"
|
||||||
|
:min="0" :max="65535" class="w-8" fluid />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||||
|
<div class="flex flex-column gap-2 grow p-fluid">
|
||||||
|
<label for="listener_urls">{{ t('listener_urls') }}</label>
|
||||||
|
<AutoComplete id="listener_urls" v-model="curNetwork.listener_urls" :suggestions="listenerSuggestions"
|
||||||
|
class="w-full" dropdown :complete-on-focus="true"
|
||||||
|
:placeholder="t('chips_placeholder', ['tcp://1.1.1.1:11010'])" multiple
|
||||||
|
@complete="searchListenerSuggestiong" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||||
|
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||||
|
<label for="rpc_port">{{ t('rpc_port') }}</label>
|
||||||
|
<InputNumber id="rpc_port" v-model="curNetwork.rpc_port" aria-describedby="rpc_port-help"
|
||||||
|
:format="false" :min="0" :max="65535" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||||
|
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||||
|
<label for="dev_name">{{ t('dev_name') }}</label>
|
||||||
|
<InputText id="dev_name" v-model="curNetwork.dev_name" aria-describedby="dev_name-help" :format="true"
|
||||||
|
:placeholder="t('dev_name_placeholder')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<div class="flex pt-4 justify-content-center">
|
||||||
|
<Button :label="t('run_network')" icon="pi pi-arrow-right" icon-pos="right" :disabled="configInvalid"
|
||||||
|
@click="$emit('runNetwork', curNetwork)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -1,8 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from 'vue-i18n';
|
import { EventType } from '~/types/network'
|
||||||
import { EventType } from '../types/network'
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import { Fieldset } from 'primevue';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
event: {
|
event: {
|
459
easytier-gui/src/components/Status.vue
Normal file
459
easytier-gui/src/components/Status.vue
Normal file
|
@ -0,0 +1,459 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useTimeAgo } from '@vueuse/core'
|
||||||
|
import { IPv4, IPv6 } from 'ip-num/IPNumber'
|
||||||
|
import type { NodeInfo, PeerRoutePair } from '~/types/network'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
instanceId?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const networkStore = useNetworkStore()
|
||||||
|
|
||||||
|
const curNetwork = computed(() => {
|
||||||
|
if (props.instanceId) {
|
||||||
|
// console.log('instanceId', props.instanceId)
|
||||||
|
const c = networkStore.networkList.find(n => n.instance_id === props.instanceId)
|
||||||
|
if (c !== undefined)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
return networkStore.curNetwork
|
||||||
|
})
|
||||||
|
|
||||||
|
const curNetworkInst = computed(() => {
|
||||||
|
return networkStore.networkInstances.find(n => n.instance_id === curNetwork.value.instance_id)
|
||||||
|
})
|
||||||
|
|
||||||
|
const peerRouteInfos = computed(() => {
|
||||||
|
if (curNetworkInst.value) {
|
||||||
|
const my_node_info = curNetworkInst.value.detail?.my_node_info
|
||||||
|
return [{
|
||||||
|
route: {
|
||||||
|
ipv4_addr: my_node_info?.virtual_ipv4,
|
||||||
|
hostname: my_node_info?.hostname,
|
||||||
|
version: my_node_info?.version,
|
||||||
|
},
|
||||||
|
}, ...(curNetworkInst.value.detail?.peer_route_pairs || [])]
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
|
||||||
|
function routeCost(info: any) {
|
||||||
|
if (info.route) {
|
||||||
|
const cost = info.route.cost
|
||||||
|
return cost ? cost === 1 ? 'p2p' : `relay(${cost})` : t('status.local')
|
||||||
|
}
|
||||||
|
|
||||||
|
return '?'
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveObjPath(path: string, obj = globalThis, separator = '.') {
|
||||||
|
const properties = Array.isArray(path) ? path : path.split(separator)
|
||||||
|
return properties.reduce((prev, curr) => prev?.[curr], obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
function statsCommon(info: any, field: string): number | undefined {
|
||||||
|
if (!info.peer)
|
||||||
|
return undefined
|
||||||
|
|
||||||
|
const conns = info.peer.conns
|
||||||
|
return conns.reduce((acc: number, conn: any) => {
|
||||||
|
return acc + resolveObjPath(field, conn)
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function humanFileSize(bytes: number, si = false, dp = 1) {
|
||||||
|
const thresh = si ? 1000 : 1024
|
||||||
|
|
||||||
|
if (Math.abs(bytes) < thresh)
|
||||||
|
return `${bytes} B`
|
||||||
|
|
||||||
|
const units = si
|
||||||
|
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||||
|
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
|
||||||
|
let u = -1
|
||||||
|
const r = 10 ** dp
|
||||||
|
|
||||||
|
do {
|
||||||
|
bytes /= thresh
|
||||||
|
++u
|
||||||
|
} while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1)
|
||||||
|
|
||||||
|
return `${bytes.toFixed(dp)} ${units[u]}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function latencyMs(info: PeerRoutePair) {
|
||||||
|
let lat_us_sum = statsCommon(info, 'stats.latency_us')
|
||||||
|
if (lat_us_sum === undefined)
|
||||||
|
return ''
|
||||||
|
lat_us_sum = lat_us_sum / 1000 / info.peer!.conns.length
|
||||||
|
return `${lat_us_sum % 1 > 0 ? Math.round(lat_us_sum) + 1 : Math.round(lat_us_sum)}ms`
|
||||||
|
}
|
||||||
|
|
||||||
|
function txBytes(info: PeerRoutePair) {
|
||||||
|
const tx = statsCommon(info, 'stats.tx_bytes')
|
||||||
|
return tx ? humanFileSize(tx) : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function rxBytes(info: PeerRoutePair) {
|
||||||
|
const rx = statsCommon(info, 'stats.rx_bytes')
|
||||||
|
return rx ? humanFileSize(rx) : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function lossRate(info: PeerRoutePair) {
|
||||||
|
const lossRate = statsCommon(info, 'loss_rate')
|
||||||
|
return lossRate !== undefined ? `${Math.round(lossRate * 100)}%` : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function version(info: PeerRoutePair) {
|
||||||
|
return info.route.version === '' ? 'unknown' : info.route.version
|
||||||
|
}
|
||||||
|
|
||||||
|
function ipFormat(info: PeerRoutePair) {
|
||||||
|
const ip = info.route.ipv4_addr
|
||||||
|
if (typeof ip === 'string')
|
||||||
|
return ip
|
||||||
|
return ip ? `${num2ipv4(ip.address)}/${ip.network_length}` : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const myNodeInfo = computed(() => {
|
||||||
|
if (!curNetworkInst.value)
|
||||||
|
return {} as NodeInfo
|
||||||
|
|
||||||
|
return curNetworkInst.value.detail?.my_node_info
|
||||||
|
})
|
||||||
|
|
||||||
|
interface Chip {
|
||||||
|
label: string
|
||||||
|
icon: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const myNodeInfoChips = computed(() => {
|
||||||
|
if (!curNetworkInst.value)
|
||||||
|
return []
|
||||||
|
|
||||||
|
const chips: Array<Chip> = []
|
||||||
|
const my_node_info = curNetworkInst.value.detail?.my_node_info
|
||||||
|
if (!my_node_info)
|
||||||
|
return chips
|
||||||
|
|
||||||
|
// TUN Device Name
|
||||||
|
const dev_name = curNetworkInst.value.detail?.dev_name
|
||||||
|
if (dev_name) {
|
||||||
|
chips.push({
|
||||||
|
label: `TUN Device Name: ${dev_name}`,
|
||||||
|
icon: '',
|
||||||
|
} as Chip)
|
||||||
|
}
|
||||||
|
|
||||||
|
// virtual ipv4
|
||||||
|
chips.push({
|
||||||
|
label: `Virtual IPv4: ${my_node_info.virtual_ipv4}`,
|
||||||
|
icon: '',
|
||||||
|
} as Chip)
|
||||||
|
|
||||||
|
// local ipv4s
|
||||||
|
const local_ipv4s = my_node_info.ips?.interface_ipv4s
|
||||||
|
for (const [idx, ip] of local_ipv4s?.entries()) {
|
||||||
|
chips.push({
|
||||||
|
label: `Local IPv4 ${idx}: ${num2ipv4(ip)}`,
|
||||||
|
icon: '',
|
||||||
|
} as Chip)
|
||||||
|
}
|
||||||
|
|
||||||
|
// local ipv6s
|
||||||
|
const local_ipv6s = my_node_info.ips?.interface_ipv6s
|
||||||
|
for (const [idx, ip] of local_ipv6s?.entries()) {
|
||||||
|
chips.push({
|
||||||
|
label: `Local IPv6 ${idx}: ${num2ipv6(ip)}`,
|
||||||
|
icon: '',
|
||||||
|
} as Chip)
|
||||||
|
}
|
||||||
|
|
||||||
|
// public ip
|
||||||
|
const public_ip = my_node_info.ips?.public_ipv4
|
||||||
|
if (public_ip) {
|
||||||
|
chips.push({
|
||||||
|
label: `Public IP: ${IPv4.fromNumber(public_ip.addr)}`,
|
||||||
|
icon: '',
|
||||||
|
} as Chip)
|
||||||
|
}
|
||||||
|
|
||||||
|
const public_ipv6 = my_node_info.ips?.public_ipv6
|
||||||
|
if (public_ipv6) {
|
||||||
|
chips.push({
|
||||||
|
label: `Public IPv6: ${IPv6.fromBigInt((BigInt(public_ipv6.part1) << BigInt(96))
|
||||||
|
+ (BigInt(public_ipv6.part2) << BigInt(64))
|
||||||
|
+ (BigInt(public_ipv6.part3) << BigInt(32))
|
||||||
|
+ BigInt(public_ipv6.part4),
|
||||||
|
)}`,
|
||||||
|
icon: '',
|
||||||
|
} as Chip)
|
||||||
|
}
|
||||||
|
|
||||||
|
// listeners:
|
||||||
|
const listeners = my_node_info.listeners
|
||||||
|
for (const [idx, listener] of listeners?.entries()) {
|
||||||
|
chips.push({
|
||||||
|
label: `Listener ${idx}: ${listener}`,
|
||||||
|
icon: '',
|
||||||
|
} as Chip)
|
||||||
|
}
|
||||||
|
|
||||||
|
// udp nat type
|
||||||
|
enum NatType {
|
||||||
|
// has NAT; but own a single public IP, port is not changed
|
||||||
|
Unknown = 0,
|
||||||
|
OpenInternet = 1,
|
||||||
|
NoPAT = 2,
|
||||||
|
FullCone = 3,
|
||||||
|
Restricted = 4,
|
||||||
|
PortRestricted = 5,
|
||||||
|
Symmetric = 6,
|
||||||
|
SymUdpFirewall = 7,
|
||||||
|
SymmetricEasyInc = 8,
|
||||||
|
SymmetricEasyDec = 9,
|
||||||
|
};
|
||||||
|
const udpNatType: NatType = my_node_info.stun_info?.udp_nat_type
|
||||||
|
if (udpNatType !== undefined) {
|
||||||
|
const udpNatTypeStrMap = {
|
||||||
|
[NatType.Unknown]: 'Unknown',
|
||||||
|
[NatType.OpenInternet]: 'Open Internet',
|
||||||
|
[NatType.NoPAT]: 'No PAT',
|
||||||
|
[NatType.FullCone]: 'Full Cone',
|
||||||
|
[NatType.Restricted]: 'Restricted',
|
||||||
|
[NatType.PortRestricted]: 'Port Restricted',
|
||||||
|
[NatType.Symmetric]: 'Symmetric',
|
||||||
|
[NatType.SymUdpFirewall]: 'Symmetric UDP Firewall',
|
||||||
|
[NatType.SymmetricEasyInc]: 'Symmetric Easy Inc',
|
||||||
|
[NatType.SymmetricEasyDec]: 'Symmetric Easy Dec',
|
||||||
|
}
|
||||||
|
|
||||||
|
chips.push({
|
||||||
|
label: `UDP NAT Type: ${udpNatTypeStrMap[udpNatType]}`,
|
||||||
|
icon: '',
|
||||||
|
} as Chip)
|
||||||
|
}
|
||||||
|
|
||||||
|
return chips
|
||||||
|
})
|
||||||
|
|
||||||
|
function globalSumCommon(field: string) {
|
||||||
|
let sum = 0
|
||||||
|
if (!peerRouteInfos.value)
|
||||||
|
return sum
|
||||||
|
|
||||||
|
for (const info of peerRouteInfos.value) {
|
||||||
|
const tx = statsCommon(info, field)
|
||||||
|
if (tx)
|
||||||
|
sum += tx
|
||||||
|
}
|
||||||
|
return sum
|
||||||
|
}
|
||||||
|
|
||||||
|
function txGlobalSum() {
|
||||||
|
return globalSumCommon('stats.tx_bytes')
|
||||||
|
}
|
||||||
|
|
||||||
|
function rxGlobalSum() {
|
||||||
|
return globalSumCommon('stats.rx_bytes')
|
||||||
|
}
|
||||||
|
|
||||||
|
const peerCount = computed(() => {
|
||||||
|
if (!peerRouteInfos.value)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
return peerRouteInfos.value.length
|
||||||
|
})
|
||||||
|
|
||||||
|
// calculate tx/rx rate every 2 seconds
|
||||||
|
let rateIntervalId = 0
|
||||||
|
const rateInterval = 2000
|
||||||
|
let prevTxSum = 0
|
||||||
|
let prevRxSum = 0
|
||||||
|
const txRate = ref('0')
|
||||||
|
const rxRate = ref('0')
|
||||||
|
onMounted(() => {
|
||||||
|
rateIntervalId = window.setInterval(() => {
|
||||||
|
const curTxSum = txGlobalSum()
|
||||||
|
txRate.value = humanFileSize((curTxSum - prevTxSum) / (rateInterval / 1000))
|
||||||
|
prevTxSum = curTxSum
|
||||||
|
|
||||||
|
const curRxSum = rxGlobalSum()
|
||||||
|
rxRate.value = humanFileSize((curRxSum - prevRxSum) / (rateInterval / 1000))
|
||||||
|
prevRxSum = curRxSum
|
||||||
|
}, rateInterval)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearInterval(rateIntervalId)
|
||||||
|
})
|
||||||
|
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const dialogContent = ref<any>('')
|
||||||
|
const dialogHeader = ref('event_log')
|
||||||
|
|
||||||
|
function showVpnPortalConfig() {
|
||||||
|
const my_node_info = myNodeInfo.value
|
||||||
|
if (!my_node_info)
|
||||||
|
return
|
||||||
|
|
||||||
|
const url = 'https://www.wireguardconfig.com/qrcode'
|
||||||
|
dialogContent.value = `${my_node_info.vpn_portal_cfg}\n\n # can generate QR code: ${url}`
|
||||||
|
dialogHeader.value = 'vpn_portal_config'
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function showEventLogs() {
|
||||||
|
const detail = curNetworkInst.value?.detail
|
||||||
|
if (!detail)
|
||||||
|
return
|
||||||
|
|
||||||
|
dialogContent.value = detail.events
|
||||||
|
dialogHeader.value = 'event_log'
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Dialog v-model:visible="dialogVisible" modal :header="t(dialogHeader)" class="w-2/3 h-auto">
|
||||||
|
<ScrollPanel v-if="dialogHeader === 'vpn_portal_config'">
|
||||||
|
<pre>{{ dialogContent }}</pre>
|
||||||
|
</ScrollPanel>
|
||||||
|
<Timeline v-else :value="dialogContent">
|
||||||
|
<template #opposite="slotProps">
|
||||||
|
<small class="text-surface-500 dark:text-surface-400">{{ useTimeAgo(Date.parse(slotProps.item[0])) }}</small>
|
||||||
|
</template>
|
||||||
|
<template #content="slotProps">
|
||||||
|
<HumanEvent :event="slotProps.item[1]" />
|
||||||
|
</template>
|
||||||
|
</Timeline>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Card v-if="curNetworkInst?.error_msg">
|
||||||
|
<template #title>
|
||||||
|
Run Network Error
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="flex flex-column gap-y-5">
|
||||||
|
<div class="text-red-500">
|
||||||
|
{{ curNetworkInst.error_msg }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<Card>
|
||||||
|
<template #title>
|
||||||
|
{{ t('my_node_info') }}
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="flex w-full flex-column gap-y-5">
|
||||||
|
<div class="m-0 flex flex-row justify-center gap-x-5">
|
||||||
|
<div
|
||||||
|
class="rounded-full w-32 h-32 flex flex-column align-items-center pt-4"
|
||||||
|
style="border: 1px solid green"
|
||||||
|
>
|
||||||
|
<div class="font-bold">
|
||||||
|
{{ t('peer_count') }}
|
||||||
|
</div>
|
||||||
|
<div class="text-5xl mt-1">
|
||||||
|
{{ peerCount }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="rounded-full w-32 h-32 flex flex-column align-items-center pt-4"
|
||||||
|
style="border: 1px solid purple"
|
||||||
|
>
|
||||||
|
<div class="font-bold">
|
||||||
|
{{ t('upload') }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xl mt-2">
|
||||||
|
{{ txRate }}/s
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="rounded-full w-32 h-32 flex flex-column align-items-center pt-4"
|
||||||
|
style="border: 1px solid fuchsia"
|
||||||
|
>
|
||||||
|
<div class="font-bold">
|
||||||
|
{{ t('download') }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xl mt-2">
|
||||||
|
{{ rxRate }}/s
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row align-items-center flex-wrap w-full max-h-40 overflow-scroll">
|
||||||
|
<Chip
|
||||||
|
v-for="(chip, i) in myNodeInfoChips" :key="i" :label="chip.label" :icon="chip.icon"
|
||||||
|
class="mr-2 mt-2 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="myNodeInfo" class="m-0 flex flex-row justify-center gap-x-5 text-sm">
|
||||||
|
<Button severity="info" :label="t('show_vpn_portal_config')" @click="showVpnPortalConfig" />
|
||||||
|
<Button severity="info" :label="t('show_event_log')" @click="showEventLogs" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<template #title>
|
||||||
|
{{ t('peer_info') }}
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<DataTable :value="peerRouteInfos" column-resize-mode="fit" table-class="w-full">
|
||||||
|
<Column :field="ipFormat" :header="t('virtual_ipv4')" />
|
||||||
|
<Column :header="t('hostname')">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<div
|
||||||
|
v-if="!slotProps.data.route.cost || !slotProps.data.route.feature_flag.is_public_server"
|
||||||
|
v-tooltip="slotProps.data.route.hostname"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
slotProps.data.route.hostname }}
|
||||||
|
</div>
|
||||||
|
<div v-else v-tooltip="slotProps.data.route.hostname" class="space-x-1">
|
||||||
|
<Tag v-if="slotProps.data.route.feature_flag.is_public_server" severity="info" value="Info">
|
||||||
|
{{ t('status.server') }}
|
||||||
|
</Tag>
|
||||||
|
<Tag v-if="slotProps.data.route.no_relay_data" severity="warn" value="Warn">
|
||||||
|
{{ t('status.relay') }}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column :field="routeCost" :header="t('route_cost')" />
|
||||||
|
<Column :field="latencyMs" :header="t('latency')" />
|
||||||
|
<Column :field="txBytes" :header="t('upload_bytes')" />
|
||||||
|
<Column :field="rxBytes" :header="t('download_bytes')" />
|
||||||
|
<Column :field="lossRate" :header="t('loss_rate')" />
|
||||||
|
<Column :header="t('status.version')">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<span>{{ version(slotProps.data) }}</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="postcss" scoped>
|
||||||
|
.p-timeline :deep(.p-timeline-event-opposite) {
|
||||||
|
@apply flex-none;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,8 +1,6 @@
|
||||||
import { addPluginListener } from '@tauri-apps/api/core'
|
import { addPluginListener } from '@tauri-apps/api/core'
|
||||||
import { prepare_vpn, start_vpn, stop_vpn } from 'tauri-plugin-vpnservice-api'
|
import { prepare_vpn, start_vpn, stop_vpn } from 'tauri-plugin-vpnservice-api'
|
||||||
import { NetworkTypes, Utils } from 'easytier-frontend-lib'
|
import type { Route } from '~/types/network'
|
||||||
|
|
||||||
type Route = NetworkTypes.Route
|
|
||||||
|
|
||||||
const networkStore = useNetworkStore()
|
const networkStore = useNetworkStore()
|
||||||
|
|
||||||
|
@ -124,17 +122,12 @@ async function onNetworkInstanceChange() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const virtual_ip = Utils.ipv4ToString(curNetworkInfo?.my_node_info?.virtual_ipv4.address)
|
const virtual_ip = curNetworkInfo?.node_info?.virtual_ipv4
|
||||||
if (!virtual_ip || !virtual_ip.length) {
|
if (!virtual_ip || !virtual_ip.length) {
|
||||||
await doStopVpn()
|
await doStopVpn()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let network_length = curNetworkInfo?.my_node_info?.virtual_ipv4.network_length
|
|
||||||
if (!network_length) {
|
|
||||||
network_length = 24
|
|
||||||
}
|
|
||||||
|
|
||||||
const routes = getRoutesForVpn(curNetworkInfo?.routes)
|
const routes = getRoutesForVpn(curNetworkInfo?.routes)
|
||||||
|
|
||||||
const ipChanged = virtual_ip !== curVpnStatus.ipv4Addr
|
const ipChanged = virtual_ip !== curVpnStatus.ipv4Addr
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import { NetworkTypes } from 'easytier-frontend-lib'
|
|
||||||
|
|
||||||
type NetworkConfig = NetworkTypes.NetworkConfig
|
import type { NetworkConfig, NetworkInstanceRunningInfo } from '~/types/network'
|
||||||
type NetworkInstanceRunningInfo = NetworkTypes.NetworkInstanceRunningInfo
|
|
||||||
|
|
||||||
export async function parseNetworkConfig(cfg: NetworkConfig) {
|
export async function parseNetworkConfig(cfg: NetworkConfig) {
|
||||||
return invoke<string>('parse_network_config', { cfg })
|
return invoke<string>('parse_network_config', { cfg })
|
||||||
|
|
15
easytier-gui/src/composables/utils.ts
Normal file
15
easytier-gui/src/composables/utils.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { IPv4, IPv6 } from 'ip-num/IPNumber'
|
||||||
|
import type { Ipv4Addr, Ipv6Addr } from '~/types/network'
|
||||||
|
|
||||||
|
export function num2ipv4(ip: Ipv4Addr) {
|
||||||
|
return IPv4.fromNumber(ip.addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function num2ipv6(ip: Ipv6Addr) {
|
||||||
|
return IPv6.fromBigInt(
|
||||||
|
(BigInt(ip.part1) << BigInt(96))
|
||||||
|
+ (BigInt(ip.part2) << BigInt(64))
|
||||||
|
+ (BigInt(ip.part3) << BigInt(32))
|
||||||
|
+ BigInt(ip.part4),
|
||||||
|
)
|
||||||
|
}
|
|
@ -5,11 +5,12 @@ import ToastService from 'primevue/toastservice'
|
||||||
import { createRouter, createWebHistory } from 'vue-router/auto'
|
import { createRouter, createWebHistory } from 'vue-router/auto'
|
||||||
import { routes } from 'vue-router/auto-routes'
|
import { routes } from 'vue-router/auto-routes'
|
||||||
import App from '~/App.vue'
|
import App from '~/App.vue'
|
||||||
import EasyTierFrontendLib, { I18nUtils } from 'easytier-frontend-lib'
|
import { i18n, loadLanguageAsync } from '~/modules/i18n'
|
||||||
|
|
||||||
import { getAutoLaunchStatusAsync, loadAutoLaunchStatusAsync } from './modules/auto_launch'
|
import { getAutoLaunchStatusAsync, loadAutoLaunchStatusAsync } from './modules/auto_launch'
|
||||||
import '~/styles.css'
|
import '~/styles.css'
|
||||||
import 'easytier-frontend-lib/style.css'
|
import 'primeicons/primeicons.css'
|
||||||
|
import 'primeflex/primeflex.css'
|
||||||
|
|
||||||
if (import.meta.env.PROD) {
|
if (import.meta.env.PROD) {
|
||||||
document.addEventListener('keydown', (event) => {
|
document.addEventListener('keydown', (event) => {
|
||||||
|
@ -28,7 +29,7 @@ if (import.meta.env.PROD) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
await I18nUtils.loadLanguageAsync(localStorage.getItem('lang') || 'en')
|
await loadLanguageAsync(localStorage.getItem('lang') || 'en')
|
||||||
await loadAutoLaunchStatusAsync(getAutoLaunchStatusAsync())
|
await loadAutoLaunchStatusAsync(getAutoLaunchStatusAsync())
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
@ -40,18 +41,14 @@ async function main() {
|
||||||
|
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.use(createPinia())
|
app.use(createPinia())
|
||||||
app.use(EasyTierFrontendLib)
|
app.use(i18n, { useScope: 'global' })
|
||||||
// app.use(i18n, { useScope: 'global' })
|
|
||||||
app.use(PrimeVue, {
|
app.use(PrimeVue, {
|
||||||
theme: {
|
theme: {
|
||||||
preset: Aura,
|
preset: Aura,
|
||||||
options: {
|
options: {
|
||||||
prefix: 'p',
|
prefix: 'p',
|
||||||
darkModeSelector: 'system',
|
darkModeSelector: 'system',
|
||||||
cssLayer: {
|
cssLayer: false,
|
||||||
name: 'primevue',
|
|
||||||
order: 'tailwind-base, primevue, tailwind-utilities'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
50
easytier-gui/src/modules/i18n.ts
Normal file
50
easytier-gui/src/modules/i18n.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import { createI18n } from 'vue-i18n'
|
||||||
|
import type { Locale } from 'vue-i18n'
|
||||||
|
|
||||||
|
// Import i18n resources
|
||||||
|
// https://vitejs.dev/guide/features.html#glob-import
|
||||||
|
export const i18n = createI18n({
|
||||||
|
legacy: false,
|
||||||
|
locale: '',
|
||||||
|
fallbackLocale: '',
|
||||||
|
messages: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
const localesMap = Object.fromEntries(
|
||||||
|
Object.entries(import.meta.glob('../../locales/*.yml'))
|
||||||
|
.map(([path, loadLocale]) => [path.match(/([\w-]*)\.yml$/)?.[1], loadLocale]),
|
||||||
|
) as Record<Locale, () => Promise<{ default: Record<string, string> }>>
|
||||||
|
|
||||||
|
export const availableLocales = Object.keys(localesMap)
|
||||||
|
|
||||||
|
const loadedLanguages: string[] = []
|
||||||
|
|
||||||
|
function setI18nLanguage(lang: Locale) {
|
||||||
|
i18n.global.locale.value = lang as any
|
||||||
|
localStorage.setItem('lang', lang)
|
||||||
|
return lang
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadLanguageAsync(lang: string): Promise<Locale> {
|
||||||
|
// If the same language
|
||||||
|
if (i18n.global.locale.value === lang)
|
||||||
|
return setI18nLanguage(lang)
|
||||||
|
|
||||||
|
// If the language was already loaded
|
||||||
|
if (loadedLanguages.includes(lang))
|
||||||
|
return setI18nLanguage(lang)
|
||||||
|
|
||||||
|
// If the language hasn't been loaded yet
|
||||||
|
let messages
|
||||||
|
|
||||||
|
try {
|
||||||
|
messages = await localesMap[lang]()
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
messages = await localesMap.en()
|
||||||
|
}
|
||||||
|
|
||||||
|
i18n.global.setLocaleMessage(lang, messages.default)
|
||||||
|
loadedLanguages.push(lang)
|
||||||
|
return setI18nLanguage(lang)
|
||||||
|
}
|
|
@ -8,11 +8,14 @@ import { exit } from '@tauri-apps/plugin-process'
|
||||||
import { open } from '@tauri-apps/plugin-shell'
|
import { open } from '@tauri-apps/plugin-shell'
|
||||||
import TieredMenu from 'primevue/tieredmenu'
|
import TieredMenu from 'primevue/tieredmenu'
|
||||||
import { useToast } from 'primevue/usetoast'
|
import { useToast } from 'primevue/usetoast'
|
||||||
import { NetworkTypes, Config, Status, Utils, I18nUtils } from 'easytier-frontend-lib'
|
import Config from '~/components/Config.vue'
|
||||||
|
|
||||||
|
import Status from '~/components/Status.vue'
|
||||||
import { isAutostart, setLoggingLevel } from '~/composables/network'
|
import { isAutostart, setLoggingLevel } from '~/composables/network'
|
||||||
import { useTray } from '~/composables/tray'
|
import { useTray } from '~/composables/tray'
|
||||||
import { getAutoLaunchStatusAsync as getAutoLaunchStatus, loadAutoLaunchStatusAsync } from '~/modules/auto_launch'
|
import { getAutoLaunchStatusAsync as getAutoLaunchStatus, loadAutoLaunchStatusAsync } from '~/modules/auto_launch'
|
||||||
|
import { loadLanguageAsync } from '~/modules/i18n'
|
||||||
|
import { type NetworkConfig, NetworkingMethod } from '~/types/network'
|
||||||
|
|
||||||
const { t, locale } = useI18n()
|
const { t, locale } = useI18n()
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
|
@ -62,27 +65,6 @@ const toast = useToast()
|
||||||
|
|
||||||
const networkStore = useNetworkStore()
|
const networkStore = useNetworkStore()
|
||||||
|
|
||||||
const curNetworkConfig = computed(() => {
|
|
||||||
if (networkStore.curNetworkId) {
|
|
||||||
// console.log('instanceId', props.instanceId)
|
|
||||||
const c = networkStore.networkList.find(n => n.instance_id === networkStore.curNetworkId)
|
|
||||||
if (c !== undefined)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
return networkStore.curNetwork
|
|
||||||
})
|
|
||||||
|
|
||||||
const curNetworkInst = computed<NetworkTypes.NetworkInstance | null>(() => {
|
|
||||||
let ret = networkStore.networkInstances.find(n => n.instance_id === curNetworkConfig.value.instance_id)
|
|
||||||
console.log('curNetworkInst', ret)
|
|
||||||
if (ret === undefined) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function addNewNetwork() {
|
function addNewNetwork() {
|
||||||
networkStore.addNewNetwork()
|
networkStore.addNewNetwork()
|
||||||
networkStore.curNetwork = networkStore.lastNetwork
|
networkStore.curNetwork = networkStore.lastNetwork
|
||||||
|
@ -100,7 +82,7 @@ networkStore.$subscribe(async () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
async function runNetworkCb(cfg: NetworkTypes.NetworkConfig, cb: () => void) {
|
async function runNetworkCb(cfg: NetworkConfig, cb: () => void) {
|
||||||
if (type() === 'android') {
|
if (type() === 'android') {
|
||||||
await prepareVpnService()
|
await prepareVpnService()
|
||||||
networkStore.clearNetworkInstances()
|
networkStore.clearNetworkInstances()
|
||||||
|
@ -124,7 +106,7 @@ async function runNetworkCb(cfg: NetworkTypes.NetworkConfig, cb: () => void) {
|
||||||
cb()
|
cb()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function stopNetworkCb(cfg: NetworkTypes.NetworkConfig, cb: () => void) {
|
async function stopNetworkCb(cfg: NetworkConfig, cb: () => void) {
|
||||||
// console.log('stopNetworkCb', cfg, cb)
|
// console.log('stopNetworkCb', cfg, cb)
|
||||||
cb()
|
cb()
|
||||||
networkStore.removeNetworkInstance(cfg.instance_id)
|
networkStore.removeNetworkInstance(cfg.instance_id)
|
||||||
|
@ -163,7 +145,7 @@ const setting_menu_items = ref([
|
||||||
label: () => t('exchange_language'),
|
label: () => t('exchange_language'),
|
||||||
icon: 'pi pi-language',
|
icon: 'pi pi-language',
|
||||||
command: async () => {
|
command: async () => {
|
||||||
await I18nUtils.loadLanguageAsync((locale.value === 'en' ? 'cn' : 'en'))
|
await loadLanguageAsync((locale.value === 'en' ? 'cn' : 'en'))
|
||||||
await setTrayMenu([
|
await setTrayMenu([
|
||||||
await MenuItemExit(t('tray.exit')),
|
await MenuItemExit(t('tray.exit')),
|
||||||
await MenuItemShow(t('tray.show')),
|
await MenuItemShow(t('tray.show')),
|
||||||
|
@ -239,7 +221,7 @@ onBeforeMount(async () => {
|
||||||
getCurrentWindow().hide()
|
getCurrentWindow().hide()
|
||||||
const autoStartIds = networkStore.autoStartInstIds
|
const autoStartIds = networkStore.autoStartInstIds
|
||||||
for (const id of autoStartIds) {
|
for (const id of autoStartIds) {
|
||||||
const cfg = networkStore.networkList.find((item: NetworkTypes.NetworkConfig) => item.instance_id === id)
|
const cfg = networkStore.networkList.find(item => item.instance_id === id)
|
||||||
if (cfg) {
|
if (cfg) {
|
||||||
networkStore.addNetworkInstance(cfg.instance_id)
|
networkStore.addNetworkInstance(cfg.instance_id)
|
||||||
await runNetworkInstance(cfg)
|
await runNetworkInstance(cfg)
|
||||||
|
@ -263,7 +245,7 @@ function isRunning(id: string) {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div id="root" class="flex flex-col">
|
<div id="root" class="flex flex-column">
|
||||||
<Dialog v-model:visible="visible" modal header="Config File" :style="{ width: '70%' }">
|
<Dialog v-model:visible="visible" modal header="Config File" :style="{ width: '70%' }">
|
||||||
<Panel>
|
<Panel>
|
||||||
<ScrollPanel style="width: 100%; height: 300px">
|
<ScrollPanel style="width: 100%; height: 300px">
|
||||||
|
@ -271,7 +253,7 @@ function isRunning(id: string) {
|
||||||
</ScrollPanel>
|
</ScrollPanel>
|
||||||
</Panel>
|
</Panel>
|
||||||
<Divider />
|
<Divider />
|
||||||
<div class="flex gap-2 justify-end">
|
<div class="flex gap-2 justify-content-end">
|
||||||
<Button type="button" :label="t('close')" @click="visible = false" />
|
<Button type="button" :label="t('close')" @click="visible = false" />
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
@ -283,55 +265,65 @@ function isRunning(id: string) {
|
||||||
<div>
|
<div>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<template #start>
|
<template #start>
|
||||||
<div class="flex items-center">
|
<div class="flex align-items-center">
|
||||||
<Button icon="pi pi-plus" severity="primary" :label="t('add_new_network')" @click="addNewNetwork" />
|
<Button icon="pi pi-plus" severity="primary" :label="t('add_new_network')" @click="addNewNetwork" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #center>
|
<template #center>
|
||||||
<div class="min-w-40">
|
<div class="min-w-40">
|
||||||
<Select v-model="networkStore.curNetwork" :options="networkStore.networkList" :highlight-on-select="false"
|
<Dropdown
|
||||||
:placeholder="t('select_network')" class="w-full">
|
v-model="networkStore.curNetwork" :options="networkStore.networkList" :highlight-on-select="false"
|
||||||
|
:placeholder="t('select_network')" class="w-full"
|
||||||
|
>
|
||||||
<template #value="slotProps">
|
<template #value="slotProps">
|
||||||
<div class="flex items-start content-center">
|
<div class="flex items-start content-center">
|
||||||
<div class="mr-4 flex-col">
|
<div class="mr-3 flex-column">
|
||||||
<span>{{ slotProps.value.network_name }}</span>
|
<span>{{ slotProps.value.network_name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<Tag class="my-auto leading-3" :severity="isRunning(slotProps.value.instance_id) ? 'success' : 'info'"
|
<Tag
|
||||||
:value="t(isRunning(slotProps.value.instance_id) ? 'network_running' : 'network_stopped')" />
|
class="my-auto leading-3" :severity="isRunning(slotProps.value.instance_id) ? 'success' : 'info'"
|
||||||
|
:value="t(isRunning(slotProps.value.instance_id) ? 'network_running' : 'network_stopped')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #option="slotProps">
|
<template #option="slotProps">
|
||||||
<div class="flex flex-col items-start content-center max-w-full">
|
<div class="flex flex-col items-start content-center max-w-full">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="mr-4">
|
<div class="mr-3">
|
||||||
{{ t('network_name') }}: {{ slotProps.option.network_name }}
|
{{ t('network_name') }}: {{ slotProps.option.network_name }}
|
||||||
</div>
|
</div>
|
||||||
<Tag class="my-auto leading-3"
|
<Tag
|
||||||
|
class="my-auto leading-3"
|
||||||
:severity="isRunning(slotProps.option.instance_id) ? 'success' : 'info'"
|
:severity="isRunning(slotProps.option.instance_id) ? 'success' : 'info'"
|
||||||
:value="t(isRunning(slotProps.option.instance_id) ? 'network_running' : 'network_stopped')" />
|
:value="t(isRunning(slotProps.option.instance_id) ? 'network_running' : 'network_stopped')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="slotProps.option.networking_method !== NetworkTypes.NetworkingMethod.Standalone"
|
<div
|
||||||
class="max-w-full overflow-hidden text-ellipsis">
|
v-if="slotProps.option.networking_method !== NetworkingMethod.Standalone"
|
||||||
{{ slotProps.option.networking_method === NetworkTypes.NetworkingMethod.Manual
|
class="max-w-full overflow-hidden text-ellipsis"
|
||||||
|
>
|
||||||
|
{{ slotProps.option.networking_method === NetworkingMethod.Manual
|
||||||
? slotProps.option.peer_urls.join(', ')
|
? slotProps.option.peer_urls.join(', ')
|
||||||
: slotProps.option.public_server_url }}
|
: slotProps.option.public_server_url }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="isRunning(slotProps.option.instance_id) && networkStore.instances[slotProps.option.instance_id].detail && (!!networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4)">
|
v-if="isRunning(slotProps.option.instance_id) && networkStore.instances[slotProps.option.instance_id].detail && (networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4 !== '')"
|
||||||
{{
|
>
|
||||||
Utils.ipv4InetToString(networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4)
|
{{ networkStore.instances[slotProps.option.instance_id].detail
|
||||||
}}
|
? networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4 : '' }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Select>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #end>
|
<template #end>
|
||||||
<Button icon="pi pi-cog" severity="secondary" aria-haspopup="true" :label="t('settings')"
|
<Button
|
||||||
aria-controls="overlay_setting_menu" @click="toggle_setting_menu" />
|
icon="pi pi-cog" severity="secondary" aria-haspopup="true" :label="t('settings')"
|
||||||
|
aria-controls="overlay_setting_menu" @click="toggle_setting_menu"
|
||||||
|
/>
|
||||||
<TieredMenu id="overlay_setting_menu" ref="setting_menu" :model="setting_menu_items" :popup="true" />
|
<TieredMenu id="overlay_setting_menu" ref="setting_menu" :model="setting_menu_items" :popup="true" />
|
||||||
</template>
|
</template>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
|
@ -349,16 +341,20 @@ function isRunning(id: string) {
|
||||||
</StepList>
|
</StepList>
|
||||||
<StepPanels value="1">
|
<StepPanels value="1">
|
||||||
<StepPanel v-slot="{ activateCallback = (s: string) => { } } = {}" value="1">
|
<StepPanel v-slot="{ activateCallback = (s: string) => { } } = {}" value="1">
|
||||||
<Config :instance-id="networkStore.curNetworkId" :config-invalid="messageBarSeverity !== Severity.None"
|
<Config
|
||||||
:cur-network="curNetworkConfig" @run-network="runNetworkCb($event, () => activateCallback('2'))" />
|
:instance-id="networkStore.curNetworkId" :config-invalid="messageBarSeverity !== Severity.None"
|
||||||
|
@run-network="runNetworkCb($event, () => activateCallback('2'))"
|
||||||
|
/>
|
||||||
</StepPanel>
|
</StepPanel>
|
||||||
<StepPanel v-slot="{ activateCallback = (s: string) => { } } = {}" value="2">
|
<StepPanel v-slot="{ activateCallback = (s: string) => { } } = {}" value="2">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-column">
|
||||||
<Status :cur-network-inst="curNetworkInst" />
|
<Status :instance-id="networkStore.curNetworkId" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex pt-6 justify-center">
|
<div class="flex pt-4 justify-content-center">
|
||||||
<Button :label="t('stop_network')" severity="danger" icon="pi pi-arrow-left"
|
<Button
|
||||||
@click="stopNetworkCb(networkStore.curNetwork, () => activateCallback('1'))" />
|
:label="t('stop_network')" severity="danger" icon="pi pi-arrow-left"
|
||||||
|
@click="stopNetworkCb(networkStore.curNetwork, () => activateCallback('1'))"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</StepPanel>
|
</StepPanel>
|
||||||
</StepPanels>
|
</StepPanels>
|
||||||
|
|
|
@ -1,25 +1,26 @@
|
||||||
import { NetworkTypes } from 'easytier-frontend-lib'
|
import type { NetworkConfig, NetworkInstance, NetworkInstanceRunningInfo } from '~/types/network'
|
||||||
|
import { DEFAULT_NETWORK_CONFIG } from '~/types/network'
|
||||||
|
|
||||||
export const useNetworkStore = defineStore('networkStore', {
|
export const useNetworkStore = defineStore('networkStore', {
|
||||||
state: () => {
|
state: () => {
|
||||||
const networkList = [NetworkTypes.DEFAULT_NETWORK_CONFIG()]
|
const networkList = [DEFAULT_NETWORK_CONFIG()]
|
||||||
return {
|
return {
|
||||||
// for initially empty lists
|
// for initially empty lists
|
||||||
networkList: networkList as NetworkTypes.NetworkConfig[],
|
networkList: networkList as NetworkConfig[],
|
||||||
// for data that is not yet loaded
|
// for data that is not yet loaded
|
||||||
curNetwork: networkList[0],
|
curNetwork: networkList[0],
|
||||||
|
|
||||||
// uuid -> instance
|
// uuid -> instance
|
||||||
instances: {} as Record<string, NetworkTypes.NetworkInstance>,
|
instances: {} as Record<string, NetworkInstance>,
|
||||||
|
|
||||||
networkInfos: {} as Record<string, NetworkTypes.NetworkInstanceRunningInfo>,
|
networkInfos: {} as Record<string, NetworkInstanceRunningInfo>,
|
||||||
|
|
||||||
autoStartInstIds: [] as string[],
|
autoStartInstIds: [] as string[],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
lastNetwork(): NetworkTypes.NetworkConfig {
|
lastNetwork(): NetworkConfig {
|
||||||
return this.networkList[this.networkList.length - 1]
|
return this.networkList[this.networkList.length - 1]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -27,7 +28,7 @@ export const useNetworkStore = defineStore('networkStore', {
|
||||||
return this.curNetwork.instance_id
|
return this.curNetwork.instance_id
|
||||||
},
|
},
|
||||||
|
|
||||||
networkInstances(): Array<NetworkTypes.NetworkInstance> {
|
networkInstances(): Array<NetworkInstance> {
|
||||||
return Object.values(this.instances)
|
return Object.values(this.instances)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -38,7 +39,7 @@ export const useNetworkStore = defineStore('networkStore', {
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
addNewNetwork() {
|
addNewNetwork() {
|
||||||
this.networkList.push(NetworkTypes.DEFAULT_NETWORK_CONFIG())
|
this.networkList.push(DEFAULT_NETWORK_CONFIG())
|
||||||
},
|
},
|
||||||
|
|
||||||
delCurNetwork() {
|
delCurNetwork() {
|
||||||
|
@ -65,7 +66,7 @@ export const useNetworkStore = defineStore('networkStore', {
|
||||||
this.instances = {}
|
this.instances = {}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateWithNetworkInfos(networkInfos: Record<string, NetworkTypes.NetworkInstanceRunningInfo>) {
|
updateWithNetworkInfos(networkInfos: Record<string, NetworkInstanceRunningInfo>) {
|
||||||
this.networkInfos = networkInfos
|
this.networkInfos = networkInfos
|
||||||
for (const [instanceId, info] of Object.entries(networkInfos)) {
|
for (const [instanceId, info] of Object.entries(networkInfos)) {
|
||||||
if (this.instances[instanceId] === undefined)
|
if (this.instances[instanceId] === undefined)
|
||||||
|
@ -78,17 +79,17 @@ export const useNetworkStore = defineStore('networkStore', {
|
||||||
},
|
},
|
||||||
|
|
||||||
loadFromLocalStorage() {
|
loadFromLocalStorage() {
|
||||||
let networkList: NetworkTypes.NetworkConfig[]
|
let networkList: NetworkConfig[]
|
||||||
|
|
||||||
// if localStorage default is [{}], instanceId will be undefined
|
// if localStorage default is [{}], instanceId will be undefined
|
||||||
networkList = JSON.parse(localStorage.getItem('networkList') || '[]')
|
networkList = JSON.parse(localStorage.getItem('networkList') || '[]')
|
||||||
networkList = networkList.map((cfg) => {
|
networkList = networkList.map((cfg) => {
|
||||||
return { ...NetworkTypes.DEFAULT_NETWORK_CONFIG(), ...cfg } as NetworkTypes.NetworkConfig
|
return { ...DEFAULT_NETWORK_CONFIG(), ...cfg } as NetworkConfig
|
||||||
})
|
})
|
||||||
|
|
||||||
// prevent a empty list from localStorage, should not happen
|
// prevent a empty list from localStorage, should not happen
|
||||||
if (networkList.length === 0)
|
if (networkList.length === 0)
|
||||||
networkList = [NetworkTypes.DEFAULT_NETWORK_CONFIG()]
|
networkList = [DEFAULT_NETWORK_CONFIG()]
|
||||||
|
|
||||||
this.networkList = networkList
|
this.networkList = networkList
|
||||||
this.curNetwork = this.networkList[0]
|
this.curNetwork = this.networkList[0]
|
||||||
|
|
213
easytier-gui/src/types/network.ts
Normal file
213
easytier-gui/src/types/network.ts
Normal file
|
@ -0,0 +1,213 @@
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
export enum NetworkingMethod {
|
||||||
|
PublicServer = 'PublicServer',
|
||||||
|
Manual = 'Manual',
|
||||||
|
Standalone = 'Standalone',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NetworkConfig {
|
||||||
|
instance_id: string
|
||||||
|
|
||||||
|
dhcp: boolean
|
||||||
|
virtual_ipv4: string
|
||||||
|
network_length: number
|
||||||
|
hostname?: string
|
||||||
|
network_name: string
|
||||||
|
network_secret: string
|
||||||
|
|
||||||
|
networking_method: NetworkingMethod
|
||||||
|
|
||||||
|
public_server_url: string
|
||||||
|
peer_urls: string[]
|
||||||
|
|
||||||
|
proxy_cidrs: string[]
|
||||||
|
|
||||||
|
enable_vpn_portal: boolean
|
||||||
|
vpn_portal_listen_port: number
|
||||||
|
vpn_portal_client_network_addr: string
|
||||||
|
vpn_portal_client_network_len: number
|
||||||
|
|
||||||
|
advanced_settings: boolean
|
||||||
|
|
||||||
|
listener_urls: string[]
|
||||||
|
rpc_port: number
|
||||||
|
latency_first: boolean
|
||||||
|
|
||||||
|
dev_name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||||
|
return {
|
||||||
|
instance_id: uuidv4(),
|
||||||
|
|
||||||
|
dhcp: true,
|
||||||
|
virtual_ipv4: '',
|
||||||
|
network_length: 24,
|
||||||
|
network_name: 'easytier',
|
||||||
|
network_secret: '',
|
||||||
|
|
||||||
|
networking_method: NetworkingMethod.PublicServer,
|
||||||
|
|
||||||
|
public_server_url: 'tcp://public.easytier.top:11010',
|
||||||
|
peer_urls: [],
|
||||||
|
|
||||||
|
proxy_cidrs: [],
|
||||||
|
|
||||||
|
enable_vpn_portal: false,
|
||||||
|
vpn_portal_listen_port: 22022,
|
||||||
|
vpn_portal_client_network_addr: '',
|
||||||
|
vpn_portal_client_network_len: 24,
|
||||||
|
|
||||||
|
advanced_settings: false,
|
||||||
|
|
||||||
|
listener_urls: [
|
||||||
|
'tcp://0.0.0.0:11010',
|
||||||
|
'udp://0.0.0.0:11010',
|
||||||
|
'wg://0.0.0.0:11011',
|
||||||
|
],
|
||||||
|
rpc_port: 0,
|
||||||
|
latency_first: true,
|
||||||
|
dev_name: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NetworkInstance {
|
||||||
|
instance_id: string
|
||||||
|
|
||||||
|
running: boolean
|
||||||
|
error_msg: string
|
||||||
|
|
||||||
|
detail?: NetworkInstanceRunningInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NetworkInstanceRunningInfo {
|
||||||
|
dev_name: string
|
||||||
|
my_node_info: NodeInfo
|
||||||
|
events: Record<string, any>
|
||||||
|
node_info: NodeInfo
|
||||||
|
routes: Route[]
|
||||||
|
peers: PeerInfo[]
|
||||||
|
peer_route_pairs: PeerRoutePair[]
|
||||||
|
running: boolean
|
||||||
|
error_msg?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Ipv4Addr {
|
||||||
|
addr: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Ipv6Addr {
|
||||||
|
part1: number
|
||||||
|
part2: number
|
||||||
|
part3: number
|
||||||
|
part4: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NodeInfo {
|
||||||
|
virtual_ipv4: string
|
||||||
|
hostname: string
|
||||||
|
version: string
|
||||||
|
ips: {
|
||||||
|
public_ipv4: Ipv4Addr
|
||||||
|
interface_ipv4s: Ipv4Addr[]
|
||||||
|
public_ipv6: Ipv6Addr
|
||||||
|
interface_ipv6s: Ipv6Addr[]
|
||||||
|
listeners: {
|
||||||
|
serialization: string
|
||||||
|
scheme_end: number
|
||||||
|
username_end: number
|
||||||
|
host_start: number
|
||||||
|
host_end: number
|
||||||
|
host: any
|
||||||
|
port?: number
|
||||||
|
path_start: number
|
||||||
|
query_start?: number
|
||||||
|
fragment_start?: number
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
stun_info: StunInfo
|
||||||
|
listeners: string[]
|
||||||
|
vpn_portal_cfg?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StunInfo {
|
||||||
|
udp_nat_type: number
|
||||||
|
tcp_nat_type: number
|
||||||
|
last_update_time: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Route {
|
||||||
|
peer_id: number
|
||||||
|
ipv4_addr: {
|
||||||
|
address: Ipv4Addr
|
||||||
|
network_length: number
|
||||||
|
} | string | null
|
||||||
|
next_hop_peer_id: number
|
||||||
|
cost: number
|
||||||
|
proxy_cidrs: string[]
|
||||||
|
hostname: string
|
||||||
|
stun_info?: StunInfo
|
||||||
|
inst_id: string
|
||||||
|
version: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PeerInfo {
|
||||||
|
peer_id: number
|
||||||
|
conns: PeerConnInfo[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PeerConnInfo {
|
||||||
|
conn_id: string
|
||||||
|
my_peer_id: number
|
||||||
|
is_client: boolean
|
||||||
|
peer_id: number
|
||||||
|
features: string[]
|
||||||
|
tunnel?: TunnelInfo
|
||||||
|
stats?: PeerConnStats
|
||||||
|
loss_rate: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PeerRoutePair {
|
||||||
|
route: Route
|
||||||
|
peer?: PeerInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TunnelInfo {
|
||||||
|
tunnel_type: string
|
||||||
|
local_addr: string
|
||||||
|
remote_addr: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PeerConnStats {
|
||||||
|
rx_bytes: number
|
||||||
|
tx_bytes: number
|
||||||
|
rx_packets: number
|
||||||
|
tx_packets: number
|
||||||
|
latency_us: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum EventType {
|
||||||
|
TunDeviceReady = 'TunDeviceReady', // string
|
||||||
|
TunDeviceError = 'TunDeviceError', // string
|
||||||
|
|
||||||
|
PeerAdded = 'PeerAdded', // number
|
||||||
|
PeerRemoved = 'PeerRemoved', // number
|
||||||
|
PeerConnAdded = 'PeerConnAdded', // PeerConnInfo
|
||||||
|
PeerConnRemoved = 'PeerConnRemoved', // PeerConnInfo
|
||||||
|
|
||||||
|
ListenerAdded = 'ListenerAdded', // any
|
||||||
|
ListenerAddFailed = 'ListenerAddFailed', // any, string
|
||||||
|
ListenerAcceptFailed = 'ListenerAcceptFailed', // any, string
|
||||||
|
ConnectionAccepted = 'ConnectionAccepted', // string, string
|
||||||
|
ConnectionError = 'ConnectionError', // string, string, string
|
||||||
|
|
||||||
|
Connecting = 'Connecting', // any
|
||||||
|
ConnectError = 'ConnectError', // string, string, string
|
||||||
|
|
||||||
|
VpnPortalClientConnected = 'VpnPortalClientConnected', // string, string
|
||||||
|
VpnPortalClientDisconnected = 'VpnPortalClientDisconnected', // string, string, string
|
||||||
|
|
||||||
|
DhcpIpv4Changed = 'DhcpIpv4Changed', // ipv4 | null, ipv4 | null
|
||||||
|
DhcpIpv4Conflicted = 'DhcpIpv4Conflicted', // ipv4 | null
|
||||||
|
}
|
|
@ -43,7 +43,6 @@ clap = { version = "4.4.8", features = [
|
||||||
"wrap_help",
|
"wrap_help",
|
||||||
] }
|
] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
|
||||||
uuid = { version = "1.5.0", features = [
|
uuid = { version = "1.5.0", features = [
|
||||||
"v4",
|
"v4",
|
||||||
"fast-rng",
|
"fast-rng",
|
||||||
|
|
|
@ -21,8 +21,6 @@
|
||||||
"@primevue/themes": "^4.2.1",
|
"@primevue/themes": "^4.2.1",
|
||||||
"@vueuse/core": "^11.1.0",
|
"@vueuse/core": "^11.1.0",
|
||||||
"aura": "link:@primevue\\themes\\aura",
|
"aura": "link:@primevue\\themes\\aura",
|
||||||
"axios": "^1.7.7",
|
|
||||||
"floating-vue": "^5.2",
|
|
||||||
"ip-num": "1.5.1",
|
"ip-num": "1.5.1",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
"primevue": "^4.2.1",
|
"primevue": "^4.2.1",
|
||||||
|
@ -43,6 +41,6 @@
|
||||||
"typescript": "~5.6.3",
|
"typescript": "~5.6.3",
|
||||||
"vite": "^5.4.10",
|
"vite": "^5.4.10",
|
||||||
"vite-plugin-dts": "^4.3.0",
|
"vite-plugin-dts": "^4.3.0",
|
||||||
"vue-tsc": "^2.1.10"
|
"vue-tsc": "^2.1.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
configInvalid?: boolean
|
configInvalid?: boolean
|
||||||
|
instanceId?: string
|
||||||
hostname?: string
|
hostname?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { IPv4 } from 'ip-num/IPNumber'
|
||||||
import { NetworkInstance, type NodeInfo, type PeerRoutePair } from '../types/network'
|
import { NetworkInstance, type NodeInfo, type PeerRoutePair } from '../types/network'
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||||
import { ipv4InetToString, ipv4ToString, ipv6ToString } from '../modules/utils';
|
import { num2ipv4, num2ipv6 } from '../modules/utils';
|
||||||
import { DataTable, Column, Tag, Chip, Button, Dialog, ScrollPanel, Timeline, Divider, Card, } from 'primevue';
|
import { DataTable, Column, Tag, Chip, Button, Dialog, ScrollPanel, Timeline, Divider, Card, } from 'primevue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
@ -138,7 +138,7 @@ const myNodeInfoChips = computed(() => {
|
||||||
|
|
||||||
// virtual ipv4
|
// virtual ipv4
|
||||||
chips.push({
|
chips.push({
|
||||||
label: `Virtual IPv4: ${ipv4InetToString(my_node_info.virtual_ipv4)}`,
|
label: `Virtual IPv4: ${my_node_info.virtual_ipv4}`,
|
||||||
icon: '',
|
icon: '',
|
||||||
} as Chip)
|
} as Chip)
|
||||||
|
|
||||||
|
@ -146,7 +146,7 @@ const myNodeInfoChips = computed(() => {
|
||||||
const local_ipv4s = my_node_info.ips?.interface_ipv4s
|
const local_ipv4s = my_node_info.ips?.interface_ipv4s
|
||||||
for (const [idx, ip] of local_ipv4s?.entries()) {
|
for (const [idx, ip] of local_ipv4s?.entries()) {
|
||||||
chips.push({
|
chips.push({
|
||||||
label: `Local IPv4 ${idx}: ${ipv4ToString(ip)}`,
|
label: `Local IPv4 ${idx}: ${num2ipv4(ip)}`,
|
||||||
icon: '',
|
icon: '',
|
||||||
} as Chip)
|
} as Chip)
|
||||||
}
|
}
|
||||||
|
@ -155,7 +155,7 @@ const myNodeInfoChips = computed(() => {
|
||||||
const local_ipv6s = my_node_info.ips?.interface_ipv6s
|
const local_ipv6s = my_node_info.ips?.interface_ipv6s
|
||||||
for (const [idx, ip] of local_ipv6s?.entries()) {
|
for (const [idx, ip] of local_ipv6s?.entries()) {
|
||||||
chips.push({
|
chips.push({
|
||||||
label: `Local IPv6 ${idx}: ${ipv6ToString(ip)}`,
|
label: `Local IPv6 ${idx}: ${num2ipv6(ip)}`,
|
||||||
icon: '',
|
icon: '',
|
||||||
} as Chip)
|
} as Chip)
|
||||||
}
|
}
|
||||||
|
@ -172,7 +172,7 @@ const myNodeInfoChips = computed(() => {
|
||||||
const public_ipv6 = my_node_info.ips?.public_ipv6
|
const public_ipv6 = my_node_info.ips?.public_ipv6
|
||||||
if (public_ipv6) {
|
if (public_ipv6) {
|
||||||
chips.push({
|
chips.push({
|
||||||
label: `Public IPv6: ${ipv6ToString(public_ipv6)}`,
|
label: `Public IPv6: ${num2ipv6(public_ipv6)}`,
|
||||||
icon: '',
|
icon: '',
|
||||||
} as Chip)
|
} as Chip)
|
||||||
}
|
}
|
||||||
|
@ -181,7 +181,7 @@ const myNodeInfoChips = computed(() => {
|
||||||
const listeners = my_node_info.listeners
|
const listeners = my_node_info.listeners
|
||||||
for (const [idx, listener] of listeners?.entries()) {
|
for (const [idx, listener] of listeners?.entries()) {
|
||||||
chips.push({
|
chips.push({
|
||||||
label: `Listener ${idx}: ${listener.url}`,
|
label: `Listener ${idx}: ${listener}`,
|
||||||
icon: '',
|
icon: '',
|
||||||
} as Chip)
|
} as Chip)
|
||||||
}
|
}
|
||||||
|
@ -295,7 +295,7 @@ function showEventLogs() {
|
||||||
if (!detail)
|
if (!detail)
|
||||||
return
|
return
|
||||||
|
|
||||||
dialogContent.value = detail.events.map((event: string) => JSON.parse(event))
|
dialogContent.value = detail.events
|
||||||
dialogHeader.value = 'event_log'
|
dialogHeader.value = 'event_log'
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
@ -309,11 +309,10 @@ function showEventLogs() {
|
||||||
</ScrollPanel>
|
</ScrollPanel>
|
||||||
<Timeline v-else :value="dialogContent">
|
<Timeline v-else :value="dialogContent">
|
||||||
<template #opposite="slotProps">
|
<template #opposite="slotProps">
|
||||||
<small class="text-surface-500 dark:text-surface-400">{{ useTimeAgo(Date.parse(slotProps.item.time))
|
<small class="text-surface-500 dark:text-surface-400">{{ useTimeAgo(Date.parse(slotProps.item[0])) }}</small>
|
||||||
}}</small>
|
|
||||||
</template>
|
</template>
|
||||||
<template #content="slotProps">
|
<template #content="slotProps">
|
||||||
<HumanEvent :event="slotProps.item.event" />
|
<HumanEvent :event="slotProps.item[1]" />
|
||||||
</template>
|
</template>
|
||||||
</Timeline>
|
</Timeline>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
|
@ -7,18 +7,9 @@ import PrimeVue from 'primevue/config'
|
||||||
|
|
||||||
import I18nUtils from './modules/i18n'
|
import I18nUtils from './modules/i18n'
|
||||||
import * as NetworkTypes from './types/network'
|
import * as NetworkTypes from './types/network'
|
||||||
import HumanEvent from './components/HumanEvent.vue';
|
|
||||||
|
|
||||||
// do not use primevue tooltip, it has serious memory leak issue
|
|
||||||
// https://github.com/primefaces/primevue/issues/5856
|
|
||||||
// import Tooltip from 'primevue/tooltip';
|
|
||||||
import { vTooltip } from 'floating-vue';
|
|
||||||
|
|
||||||
import * as Api from './modules/api';
|
|
||||||
import * as Utils from './modules/utils';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
install: (app: App): void => {
|
install: (app: App) => {
|
||||||
app.use(I18nUtils.i18n, { useScope: 'global' })
|
app.use(I18nUtils.i18n, { useScope: 'global' })
|
||||||
app.use(PrimeVue, {
|
app.use(PrimeVue, {
|
||||||
theme: {
|
theme: {
|
||||||
|
@ -36,9 +27,7 @@ export default {
|
||||||
|
|
||||||
app.component('Config', Config);
|
app.component('Config', Config);
|
||||||
app.component('Status', Status);
|
app.component('Status', Status);
|
||||||
app.component('HumanEvent', HumanEvent);
|
|
||||||
app.directive('tooltip', vTooltip as any);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export { Config, Status, I18nUtils, NetworkTypes, Api, Utils };
|
export { Config, Status, I18nUtils, NetworkTypes };
|
||||||
|
|
|
@ -1,18 +1,11 @@
|
||||||
import { IPv4, IPv6 } from 'ip-num/IPNumber'
|
import { IPv4, IPv6 } from 'ip-num/IPNumber'
|
||||||
import { Ipv4Addr, Ipv4Inet, Ipv6Addr } from '../types/network'
|
import { Ipv4Addr, Ipv6Addr } from '../types/network'
|
||||||
|
|
||||||
export function ipv4ToString(ip: Ipv4Addr) {
|
export function num2ipv4(ip: Ipv4Addr) {
|
||||||
return IPv4.fromNumber(ip.addr).toString()
|
return IPv4.fromNumber(ip.addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ipv4InetToString(ip: Ipv4Inet | undefined) {
|
export function num2ipv6(ip: Ipv6Addr) {
|
||||||
if (ip?.address === undefined) {
|
|
||||||
return 'undefined'
|
|
||||||
}
|
|
||||||
return `${ipv4ToString(ip.address)}/${ip.network_length}`
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ipv6ToString(ip: Ipv6Addr) {
|
|
||||||
return IPv6.fromBigInt(
|
return IPv6.fromBigInt(
|
||||||
(BigInt(ip.part1) << BigInt(96))
|
(BigInt(ip.part1) << BigInt(96))
|
||||||
+ (BigInt(ip.part2) << BigInt(64))
|
+ (BigInt(ip.part2) << BigInt(64))
|
||||||
|
@ -20,89 +13,3 @@ export function ipv6ToString(ip: Ipv6Addr) {
|
||||||
+ BigInt(ip.part4),
|
+ BigInt(ip.part4),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function toHexString(uint64: bigint, padding = 9): string {
|
|
||||||
let hexString = uint64.toString(16);
|
|
||||||
while (hexString.length < padding) {
|
|
||||||
hexString = '0' + hexString;
|
|
||||||
}
|
|
||||||
return hexString;
|
|
||||||
}
|
|
||||||
|
|
||||||
function uint32ToUuid(part1: number, part2: number, part3: number, part4: number): string {
|
|
||||||
// 将两个 uint64 转换为 16 进制字符串
|
|
||||||
const part1Hex = toHexString(BigInt(part1), 8);
|
|
||||||
const part2Hex = toHexString(BigInt(part2), 8);
|
|
||||||
const part3Hex = toHexString(BigInt(part3), 8);
|
|
||||||
const part4Hex = toHexString(BigInt(part4), 8);
|
|
||||||
|
|
||||||
// 构造 UUID 格式字符串
|
|
||||||
const uuid = `${part1Hex.substring(0, 8)}-${part2Hex.substring(0, 4)}-${part2Hex.substring(4, 8)}-${part3Hex.substring(0, 4)}-${part3Hex.substring(4, 8)}${part4Hex.substring(0, 12)}`;
|
|
||||||
|
|
||||||
return uuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UUID {
|
|
||||||
part1: number;
|
|
||||||
part2: number;
|
|
||||||
part3: number;
|
|
||||||
part4: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function UuidToStr(uuid: UUID): string {
|
|
||||||
return uint32ToUuid(uuid.part1, uuid.part2, uuid.part3, uuid.part4);
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DeviceInfo {
|
|
||||||
hostname: string;
|
|
||||||
public_ip: string;
|
|
||||||
running_network_count: number;
|
|
||||||
report_time: string;
|
|
||||||
easytier_version: string;
|
|
||||||
running_network_instances?: Array<string>;
|
|
||||||
machine_id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildDeviceInfo(device: any): DeviceInfo {
|
|
||||||
let dev_info: DeviceInfo = {
|
|
||||||
hostname: device.info?.hostname,
|
|
||||||
public_ip: device.client_url,
|
|
||||||
running_network_instances: device.info?.running_network_instances.map((instance: any) => UuidToStr(instance)),
|
|
||||||
running_network_count: device.info?.running_network_instances.length,
|
|
||||||
report_time: device.info?.report_time,
|
|
||||||
easytier_version: device.info?.easytier_version,
|
|
||||||
machine_id: UuidToStr(device.info?.machine_id),
|
|
||||||
};
|
|
||||||
|
|
||||||
return dev_info;
|
|
||||||
}
|
|
||||||
|
|
||||||
// write a class to run a function periodically and can be stopped by calling stop(), use setTimeout to trigger the function
|
|
||||||
export class PeriodicTask {
|
|
||||||
private interval: number;
|
|
||||||
private task: (() => Promise<void>) | undefined;
|
|
||||||
private timer: any;
|
|
||||||
|
|
||||||
constructor(task: () => Promise<void>, interval: number) {
|
|
||||||
this.interval = interval;
|
|
||||||
this.task = task;
|
|
||||||
}
|
|
||||||
|
|
||||||
_runTaskHelper(nextInterval: number) {
|
|
||||||
this.timer = setTimeout(async () => {
|
|
||||||
if (this.task) {
|
|
||||||
await this.task();
|
|
||||||
this._runTaskHelper(this.interval);
|
|
||||||
}
|
|
||||||
}, nextInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
this._runTaskHelper(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
this.task = undefined;
|
|
||||||
clearTimeout(this.timer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
@import 'primeicons/primeicons.css';
|
@import 'primeicons/primeicons.css';
|
||||||
@import 'floating-vue/dist/style.css';
|
|
||||||
|
|
||||||
.frontend-lib {
|
.frontend-lib {
|
||||||
|
|
||||||
|
|
|
@ -84,7 +84,8 @@ export interface NetworkInstance {
|
||||||
export interface NetworkInstanceRunningInfo {
|
export interface NetworkInstanceRunningInfo {
|
||||||
dev_name: string
|
dev_name: string
|
||||||
my_node_info: NodeInfo
|
my_node_info: NodeInfo
|
||||||
events: Array<string>,
|
events: Record<string, any>
|
||||||
|
node_info: NodeInfo
|
||||||
routes: Route[]
|
routes: Route[]
|
||||||
peers: PeerInfo[]
|
peers: PeerInfo[]
|
||||||
peer_route_pairs: PeerRoutePair[]
|
peer_route_pairs: PeerRoutePair[]
|
||||||
|
@ -96,11 +97,6 @@ export interface Ipv4Addr {
|
||||||
addr: number
|
addr: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Ipv4Inet {
|
|
||||||
address: Ipv4Addr
|
|
||||||
network_length: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Ipv6Addr {
|
export interface Ipv6Addr {
|
||||||
part1: number
|
part1: number
|
||||||
part2: number
|
part2: number
|
||||||
|
@ -108,12 +104,8 @@ export interface Ipv6Addr {
|
||||||
part4: number
|
part4: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Url {
|
|
||||||
url: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NodeInfo {
|
export interface NodeInfo {
|
||||||
virtual_ipv4: Ipv4Inet,
|
virtual_ipv4: string
|
||||||
hostname: string
|
hostname: string
|
||||||
version: string
|
version: string
|
||||||
ips: {
|
ips: {
|
||||||
|
@ -135,7 +127,7 @@ export interface NodeInfo {
|
||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
stun_info: StunInfo
|
stun_info: StunInfo
|
||||||
listeners: Url[]
|
listeners: string[]
|
||||||
vpn_portal_cfg?: string
|
vpn_portal_cfg?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,7 +139,10 @@ export interface StunInfo {
|
||||||
|
|
||||||
export interface Route {
|
export interface Route {
|
||||||
peer_id: number
|
peer_id: number
|
||||||
ipv4_addr: Ipv4Inet | string | null
|
ipv4_addr: {
|
||||||
|
address: Ipv4Addr
|
||||||
|
network_length: number
|
||||||
|
} | string | null
|
||||||
next_hop_peer_id: number
|
next_hop_peer_id: number
|
||||||
cost: number
|
cost: number
|
||||||
proxy_cidrs: string[]
|
proxy_cidrs: string[]
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/png" href="/easytier.png" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>EasyTier Dashboard</title>
|
<title>Vite + Vue + TS</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
@ -15,8 +15,7 @@
|
||||||
"easytier-frontend-lib": "workspace:*",
|
"easytier-frontend-lib": "workspace:*",
|
||||||
"primevue": "^4.2.1",
|
"primevue": "^4.2.1",
|
||||||
"tailwindcss-primeui": "^0.3.4",
|
"tailwindcss-primeui": "^0.3.4",
|
||||||
"vue": "^3.5.12",
|
"vue": "^3.5.12"
|
||||||
"vue-router": "4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.8.6",
|
"@types/node": "^22.8.6",
|
||||||
|
@ -27,6 +26,6 @@
|
||||||
"typescript": "~5.6.2",
|
"typescript": "~5.6.2",
|
||||||
"vite": "^5.4.10",
|
"vite": "^5.4.10",
|
||||||
"vite-plugin-singlefile": "^2.0.3",
|
"vite-plugin-singlefile": "^2.0.3",
|
||||||
"vue-tsc": "^2.1.10"
|
"vue-tsc": "^2.1.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 21 KiB |
1
easytier-web/frontend/public/vite.svg
Normal file
1
easytier-web/frontend/public/vite.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -2,7 +2,12 @@
|
||||||
|
|
||||||
import { I18nUtils } from 'easytier-frontend-lib'
|
import { I18nUtils } from 'easytier-frontend-lib'
|
||||||
import { onMounted } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
import { Toast, DynamicDialog } from 'primevue';
|
import Login from './components/Login.vue'
|
||||||
|
import { Button } from 'primevue';
|
||||||
|
import ApiClient from './modules/api';
|
||||||
|
import DeviceList from './components/DeviceList.vue';
|
||||||
|
|
||||||
|
const api = new ApiClient('http://10.147.223.128:11211/api/v1/'); // Replace with actual API URL
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await I18nUtils.loadLanguageAsync('cn')
|
await I18nUtils.loadLanguageAsync('cn')
|
||||||
|
@ -13,10 +18,109 @@ onMounted(async () => {
|
||||||
<!-- https://flowbite.com/docs/components/sidebar/#sidebar-with-navbar -->
|
<!-- https://flowbite.com/docs/components/sidebar/#sidebar-with-navbar -->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Toast />
|
<div id="root" class="">
|
||||||
<DynamicDialog />
|
<nav class="fixed top-0 z-50 w-full bg-white border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700">
|
||||||
|
<div class="px-3 py-3 lg:px-5 lg:pl-3">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center justify-start rtl:justify-end">
|
||||||
|
<button data-drawer-target="logo-sidebar" data-drawer-toggle="logo-sidebar" aria-controls="logo-sidebar"
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center p-2 text-sm text-gray-500 rounded-lg sm:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600">
|
||||||
|
<span class="sr-only">Open sidebar</span>
|
||||||
|
<i class="pi pi-list" style="font-size: 1.3rem"></i>
|
||||||
|
</button>
|
||||||
|
<a href="https://flowbite.com" class="flex ms-2 md:me-24">
|
||||||
|
<img src="https://flowbite.com/docs/images/logo.svg" class="h-8 me-3" alt="FlowBite Logo" />
|
||||||
|
<span
|
||||||
|
class="self-center text-xl font-semibold sm:text-2xl whitespace-nowrap dark:text-white">EasyTier</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="flex items-center ms-3">
|
||||||
|
<div>
|
||||||
|
<button type="button"
|
||||||
|
class="flex text-sm bg-gray-800 rounded-full focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600"
|
||||||
|
aria-expanded="false" data-dropdown-toggle="dropdown-user">
|
||||||
|
<span class="sr-only">Open user menu</span>
|
||||||
|
<img class="w-8 h-8 rounded-full" src="https://flowbite.com/docs/images/people/profile-picture-5.jpg"
|
||||||
|
alt="user photo">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded shadow dark:bg-gray-700 dark:divide-gray-600"
|
||||||
|
id="dropdown-user">
|
||||||
|
<div class="px-4 py-3" role="none">
|
||||||
|
<p class="text-sm text-gray-900 dark:text-white" role="none">
|
||||||
|
Neil Sims
|
||||||
|
</p>
|
||||||
|
<p class="text-sm font-medium text-gray-900 truncate dark:text-gray-300" role="none">
|
||||||
|
neil.sims@flowbite.com
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ul class="py-1" role="none">
|
||||||
|
<li>
|
||||||
|
<a href="#"
|
||||||
|
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||||
|
role="menuitem">Dashboard</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#"
|
||||||
|
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||||
|
role="menuitem">Settings</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#"
|
||||||
|
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||||
|
role="menuitem">Earnings</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#"
|
||||||
|
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||||
|
role="menuitem">Sign out</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<RouterView />
|
<aside id="logo-sidebar"
|
||||||
|
class="fixed top-0 left-0 z-40 w-64 h-screen pt-20 transition-transform -translate-x-full bg-white border-r border-gray-200 sm:translate-x-0 dark:bg-gray-800 dark:border-gray-700"
|
||||||
|
aria-label="Sidebar">
|
||||||
|
<div class="h-full px-3 pb-4 overflow-y-auto bg-white dark:bg-gray-800">
|
||||||
|
<ul class="space-y-2 font-medium">
|
||||||
|
<li>
|
||||||
|
<Button variant="text" class="w-full justify-start gap-x-3 pl-1.5" severity="contrast">
|
||||||
|
<i class="pi pi-chart-pie" style="font-size: 1.2rem"></i>
|
||||||
|
<span class="mb-0.5">DashBoard</span>
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Button variant="text" class="w-full justify-start gap-x-3 pl-1.5" severity="contrast">
|
||||||
|
<i class="pi pi-server" style="font-size: 1.2rem"></i>
|
||||||
|
<span class="mb-0.5">Devices</span>
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div class="p-4 sm:ml-64">
|
||||||
|
<div class="p-4 border-2 border-gray-200 border-dashed rounded-lg dark:border-gray-700 mt-14">
|
||||||
|
<div class="grid grid-cols-1 gap-4 mb-4">
|
||||||
|
<DeviceList :api="api"></DeviceList>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 gap-4 mb-4">
|
||||||
|
<Login :api="api"></Login>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 21 KiB |
1
easytier-web/frontend/src/assets/vue.svg
Normal file
1
easytier-web/frontend/src/assets/vue.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
After Width: | Height: | Size: 496 B |
|
@ -1,33 +0,0 @@
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, inject, ref } from 'vue';
|
|
||||||
import { Card, Password, Button } from 'primevue';
|
|
||||||
import { Api } from 'easytier-frontend-lib';
|
|
||||||
|
|
||||||
const dialogRef = inject<any>('dialogRef');
|
|
||||||
|
|
||||||
const api = computed<Api.ApiClient>(() => dialogRef.value.data.api);
|
|
||||||
|
|
||||||
const password = ref('');
|
|
||||||
|
|
||||||
const changePassword = async () => {
|
|
||||||
await api.value.change_password(password.value);
|
|
||||||
dialogRef.value.close();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex items-center justify-center">
|
|
||||||
<Card class="w-full max-w-md p-6">
|
|
||||||
<template #header>
|
|
||||||
<h2 class="text-2xl font-semibold text-center">Change Password
|
|
||||||
</h2>
|
|
||||||
</template>
|
|
||||||
<template #content>
|
|
||||||
<div class="flex flex-col space-y-4">
|
|
||||||
<Password v-model="password" placeholder="New Password" :feedback="false" toggleMask />
|
|
||||||
<Button @click="changePassword" label="Ok" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
|
@ -1,65 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { Card, useToast } from 'primevue';
|
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
|
||||||
import { Api, Utils } from 'easytier-frontend-lib';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
api: Api.ApiClient,
|
|
||||||
});
|
|
||||||
|
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
const summary = ref<Api.Summary | undefined>(undefined);
|
|
||||||
|
|
||||||
const loadSummary = async () => {
|
|
||||||
const resp = await props.api?.get_summary();
|
|
||||||
summary.value = resp;
|
|
||||||
};
|
|
||||||
|
|
||||||
const periodFunc = new Utils.PeriodicTask(async () => {
|
|
||||||
try {
|
|
||||||
await loadSummary();
|
|
||||||
} catch (e) {
|
|
||||||
toast.add({ severity: 'error', summary: 'Load Summary Failed', detail: e, life: 2000 });
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
periodFunc.start();
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
periodFunc.stop();
|
|
||||||
});
|
|
||||||
|
|
||||||
const deviceCount = computed<number | undefined>(
|
|
||||||
() => {
|
|
||||||
return summary.value?.device_count;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="grid grid-cols-3 gap-4">
|
|
||||||
<Card class="h-full">
|
|
||||||
<template #title>Device Count</template>
|
|
||||||
<template #content>
|
|
||||||
<div class="w-full flex justify-center text-7xl font-bold text-green-800 mt-4">
|
|
||||||
{{ deviceCount }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Card>
|
|
||||||
<div class="flex items-center justify-center rounded bg-gray-50 dark:bg-gray-800">
|
|
||||||
<p class="text-2xl text-gray-400 dark:text-gray-500">
|
|
||||||
<!-- <svg class="w-3.5 h-3.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
|
|
||||||
viewBox="0 0 18 18">
|
|
||||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
||||||
d="M9 1v16M1 9h16" />
|
|
||||||
</svg> -->
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</template>
|
|
|
@ -1,86 +1,204 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { Button, Column, DataTable, Drawer, ProgressSpinner, useToast } from 'primevue';
|
import ApiClient, { ValidateConfigResponse } from '../modules/api';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { Config, Status, NetworkTypes } from 'easytier-frontend-lib'
|
||||||
import { Api, Utils } from 'easytier-frontend-lib';
|
import { Button, Column, DataTable, Drawer, Toolbar, IftaLabel, Select, Dialog, ConfirmPopup, useConfirm } from 'primevue';
|
||||||
|
|
||||||
|
function toHexString(uint64: bigint, padding = 9): string {
|
||||||
|
let hexString = uint64.toString(16);
|
||||||
|
while (hexString.length < padding) {
|
||||||
|
hexString = '0' + hexString;
|
||||||
|
}
|
||||||
|
return hexString;
|
||||||
|
}
|
||||||
|
|
||||||
|
function uint32ToUuid(part1: number, part2: number, part3: number, part4: number): string {
|
||||||
|
// 将两个 uint64 转换为 16 进制字符串
|
||||||
|
const part1Hex = toHexString(BigInt(part1), 8);
|
||||||
|
const part2Hex = toHexString(BigInt(part2), 8);
|
||||||
|
const part3Hex = toHexString(BigInt(part3), 8);
|
||||||
|
const part4Hex = toHexString(BigInt(part4), 8);
|
||||||
|
|
||||||
|
// 构造 UUID 格式字符串
|
||||||
|
const uuid = `${part1Hex.substring(0, 8)}-${part2Hex.substring(0, 4)}-${part2Hex.substring(4, 8)}-${part3Hex.substring(0, 4)}-${part3Hex.substring(4, 8)}${part4Hex.substring(0, 12)}`;
|
||||||
|
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UUID {
|
||||||
|
part1: number;
|
||||||
|
part2: number;
|
||||||
|
part3: number;
|
||||||
|
part4: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function UuidToStr(uuid: UUID): string {
|
||||||
|
return uint32ToUuid(uuid.part1, uuid.part2, uuid.part3, uuid.part4);
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
api: Api.ApiClient,
|
api: ApiClient,
|
||||||
});
|
});
|
||||||
|
|
||||||
const api = props.api;
|
const api = props.api;
|
||||||
|
|
||||||
const deviceList = ref<Array<Utils.DeviceInfo> | undefined>(undefined);
|
interface DeviceList {
|
||||||
|
hostname: string;
|
||||||
|
public_ip: string;
|
||||||
|
running_network_count: number;
|
||||||
|
report_time: string;
|
||||||
|
easytier_version: string;
|
||||||
|
running_network_instances?: Array<string>;
|
||||||
|
machine_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
const selectedDeviceId = computed<string | undefined>(() => route.params.deviceId as string);
|
const selectedDevice = ref<DeviceList | null>(null);
|
||||||
|
const deviceList = ref<Array<DeviceList>>([]);
|
||||||
const route = useRoute();
|
const instanceIdList = computed(() => {
|
||||||
const router = useRouter();
|
let insts = selectedDevice.value?.running_network_instances || [];
|
||||||
const toast = useToast();
|
let options = insts.map((instance: string) => {
|
||||||
|
return { uuid: instance };
|
||||||
|
});
|
||||||
|
console.log("options", options);
|
||||||
|
return options;
|
||||||
|
});
|
||||||
|
const selectedInstanceId = ref<any | null>(null);
|
||||||
|
const curNetworkInfo = ref<NetworkTypes.NetworkInstance | null>(null);
|
||||||
|
|
||||||
const loadDevices = async () => {
|
const loadDevices = async () => {
|
||||||
const resp = await api?.list_machines();
|
const resp = await api?.list_machines();
|
||||||
let devices: Array<Utils.DeviceInfo> = [];
|
console.log(resp);
|
||||||
|
let devices: Array<DeviceList> = [];
|
||||||
for (const device of (resp || [])) {
|
for (const device of (resp || [])) {
|
||||||
devices.push({
|
devices.push({
|
||||||
hostname: device.info?.hostname,
|
hostname: device.info?.hostname,
|
||||||
public_ip: device.client_url,
|
public_ip: device.client_url,
|
||||||
running_network_instances: device.info?.running_network_instances.map((instance: any) => Utils.UuidToStr(instance)),
|
running_network_instances: device.info?.running_network_instances.map((instance: any) => UuidToStr(instance)),
|
||||||
running_network_count: device.info?.running_network_instances.length,
|
running_network_count: device.info?.running_network_instances.length,
|
||||||
report_time: device.info?.report_time,
|
report_time: device.info?.report_time,
|
||||||
easytier_version: device.info?.easytier_version,
|
easytier_version: device.info?.easytier_version,
|
||||||
machine_id: Utils.UuidToStr(device.info?.machine_id),
|
machine_id: UuidToStr(device.info?.machine_id),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.debug("device list", deviceList.value);
|
|
||||||
deviceList.value = devices;
|
deviceList.value = devices;
|
||||||
|
console.log(deviceList.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const periodFunc = new Utils.PeriodicTask(async () => {
|
interface SelectedDevice {
|
||||||
try {
|
machine_id: string;
|
||||||
await loadDevices();
|
instance_id: string;
|
||||||
} catch (e) {
|
}
|
||||||
toast.add({ severity: 'error', summary: 'Load Device List Failed', detail: e, life: 2000 });
|
|
||||||
console.error(e);
|
const checkDeviceSelected = (): SelectedDevice => {
|
||||||
|
let machine_id = selectedDevice.value?.machine_id;
|
||||||
|
let inst_id = selectedInstanceId.value?.uuid;
|
||||||
|
if (machine_id && inst_id) {
|
||||||
|
return { machine_id, instance_id: inst_id };
|
||||||
|
} else {
|
||||||
|
throw new Error("No device selected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadDeviceInfo = async () => {
|
||||||
|
let selectedDevice = checkDeviceSelected();
|
||||||
|
if (!selectedDevice) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ret = await api?.get_network_info(selectedDevice.machine_id, selectedDevice.instance_id);
|
||||||
|
let device_info = ret[selectedDevice.instance_id]
|
||||||
|
|
||||||
|
curNetworkInfo.value = {
|
||||||
|
instance_id: selectedDevice.instance_id,
|
||||||
|
running: device_info.running,
|
||||||
|
error_msg: device_info.error_msg,
|
||||||
|
detail: device_info,
|
||||||
|
} as NetworkTypes.NetworkInstance;
|
||||||
}
|
}
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
periodFunc.start();
|
setInterval(loadDevices, 1000);
|
||||||
|
setInterval(loadDeviceInfo, 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
const visibleRight = ref(false);
|
||||||
periodFunc.stop();
|
|
||||||
});
|
|
||||||
|
|
||||||
const deviceManageVisible = computed<boolean>({
|
const showCreateNetworkDialog = ref(false);
|
||||||
get: () => !!selectedDeviceId.value,
|
const newNetworkConfig = ref<NetworkTypes.NetworkConfig>(NetworkTypes.DEFAULT_NETWORK_CONFIG());
|
||||||
set: (value) => {
|
|
||||||
if (!value) {
|
const verifyNetworkConfig = async (): Promise<ValidateConfigResponse | undefined> => {
|
||||||
router.push({ name: 'deviceList', params: { deviceId: undefined } });
|
let machine_id = selectedDevice.value?.machine_id;
|
||||||
|
if (!machine_id) {
|
||||||
|
throw new Error("No machine selected");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!newNetworkConfig.value) {
|
||||||
|
throw new Error("No network config");
|
||||||
|
}
|
||||||
|
|
||||||
|
let ret = await api?.validate_config(machine_id, newNetworkConfig.value);
|
||||||
|
console.log("verifyNetworkConfig", ret);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createNewNetwork = async () => {
|
||||||
|
let config = await verifyNetworkConfig();
|
||||||
|
if (!config) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let machine_id = selectedDevice.value?.machine_id;
|
||||||
|
if (!machine_id) {
|
||||||
|
throw new Error("No machine selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
let ret = await api?.run_network(machine_id, config?.toml_config);
|
||||||
|
console.log("createNewNetwork", ret);
|
||||||
|
showCreateNetworkDialog.value = false;
|
||||||
|
await loadDevices();
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirm = useConfirm();
|
||||||
|
const confirmDeleteNetwork = (event: any) => {
|
||||||
|
confirm.require({
|
||||||
|
target: event.currentTarget,
|
||||||
|
message: 'Do you want to delete this network?',
|
||||||
|
icon: 'pi pi-info-circle',
|
||||||
|
rejectProps: {
|
||||||
|
label: 'Cancel',
|
||||||
|
severity: 'secondary',
|
||||||
|
outlined: true
|
||||||
|
},
|
||||||
|
acceptProps: {
|
||||||
|
label: 'Delete',
|
||||||
|
severity: 'danger'
|
||||||
|
},
|
||||||
|
accept: async () => {
|
||||||
|
const ret = checkDeviceSelected();
|
||||||
|
await api?.delete_network(ret?.machine_id, ret?.instance_id);
|
||||||
|
await loadDevices();
|
||||||
|
},
|
||||||
|
reject: () => {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
};
|
||||||
const selectedDeviceHostname = computed<string | undefined>(() => {
|
|
||||||
return deviceList.value?.find((device) => device.machine_id === selectedDeviceId.value)?.hostname;
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="deviceList === undefined" class="w-full flex justify-center">
|
<ConfirmPopup></ConfirmPopup>
|
||||||
<ProgressSpinner />
|
<Dialog v-model:visible="showCreateNetworkDialog" modal header="Create New Network" :style="{ width: '55rem' }">
|
||||||
</div>
|
<Config :cur-network="newNetworkConfig" @run-network="createNewNetwork"></Config>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<DataTable :value="deviceList" tableStyle="min-width: 50rem" :metaKeySelection="true" sortField="hostname"
|
<DataTable :value="deviceList" tableStyle="min-width: 50rem" :metaKeySelection="true" sortField="hostname"
|
||||||
:sortOrder="-1" v-if="deviceList !== undefined">
|
:sortOrder="-1">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="text-xl font-bold">Device List</div>
|
<div class="text-xl font-bold">Device List</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<Column field="hostname" header="Hostname" sortable style="width: 180px"></Column>
|
<Column field="hostname" header="Hostname" sortable style="width: 180px"></Column>
|
||||||
<Column field="public_ip" header="Public IP" style="width: 150px"></Column>
|
<Column field="public_ip" header="Public IP" style="width: 150px"></Column>
|
||||||
<Column field="running_network_count" header="Running Network Count" sortable style="width: 150px"></Column>
|
<Column field="running_network_count" header="Running Network Count" sortable style="width: 150px"></Column>
|
||||||
|
@ -88,23 +206,38 @@ const selectedDeviceHostname = computed<string | undefined>(() => {
|
||||||
<Column field="easytier_version" header="EasyTier Version" sortable style="width: 150px"></Column>
|
<Column field="easytier_version" header="EasyTier Version" sortable style="width: 150px"></Column>
|
||||||
<Column class="w-24 !text-end">
|
<Column class="w-24 !text-end">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
<Button icon="pi pi-cog"
|
<Button icon="pi pi-search" @click="selectedDevice = data; visibleRight = true" severity="secondary"
|
||||||
@click="router.push({ name: 'deviceManagement', params: { deviceId: data.machine_id, instanceId: data.running_network_instances[0] } })"
|
rounded></Button>
|
||||||
severity="secondary" rounded></Button>
|
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-start">
|
||||||
<Button icon="pi pi-refresh" label="Reload" severity="info" @click="loadDevices" />
|
<Button icon="pi pi-refresh" label="Reload" severity="info" @click="loadDevices" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
|
|
||||||
<Drawer v-model:visible="deviceManageVisible" :header="`Manage ${selectedDeviceHostname}`" position="right"
|
<Drawer v-model:visible="visibleRight" header="Device Management" position="right" class="w-1/2 min-w-96">
|
||||||
class="w-1/2 min-w-96">
|
<Toolbar>
|
||||||
<RouterView v-slot="{ Component }">
|
<template #start>
|
||||||
<component :is="Component" :api="api" :deviceList="deviceList" @update="loadDevices" />
|
<IftaLabel>
|
||||||
</RouterView>
|
<Select v-model="selectedInstanceId" :options="instanceIdList" optionLabel="uuid"
|
||||||
|
inputId="dd-inst-id" placeholder="Select Instance" />
|
||||||
|
<label class="mr-3" for="dd-inst-id">Network</label>
|
||||||
|
</IftaLabel>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #end>
|
||||||
|
<div class="gap-x-3 flex">
|
||||||
|
<Button @click="confirmDeleteNetwork($event)" icon="pi pi-minus" severity="danger" label="Delete"
|
||||||
|
iconPos="right" />
|
||||||
|
<Button @click="showCreateNetworkDialog = true" icon="pi pi-plus" label="Create" iconPos="right" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Toolbar>
|
||||||
|
|
||||||
|
<Status v-bind:cur-network-inst="curNetworkInfo">
|
||||||
|
|
||||||
|
</Status>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,197 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { Toolbar, IftaLabel, Select, Button, ConfirmPopup, Dialog, useConfirm, useToast } from 'primevue';
|
|
||||||
import { NetworkTypes, Status, Utils, Api, } from 'easytier-frontend-lib';
|
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
api: Api.ApiClient;
|
|
||||||
deviceList: Array<Utils.DeviceInfo> | undefined;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emits = defineEmits(['update']);
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
const router = useRouter();
|
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
const deviceId = computed<string>(() => {
|
|
||||||
return route.params.deviceId as string;
|
|
||||||
});
|
|
||||||
|
|
||||||
const instanceId = computed<string>(() => {
|
|
||||||
return route.params.instanceId as string;
|
|
||||||
});
|
|
||||||
|
|
||||||
const deviceInfo = computed<Utils.DeviceInfo | undefined | null>(() => {
|
|
||||||
return deviceId.value ? props.deviceList?.find((device) => device.machine_id === deviceId.value) : null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const curNetworkInfo = ref<NetworkTypes.NetworkInstance | null>(null);
|
|
||||||
|
|
||||||
const isEditing = ref(false);
|
|
||||||
const showCreateNetworkDialog = ref(false);
|
|
||||||
const newNetworkConfig = ref<NetworkTypes.NetworkConfig>(NetworkTypes.DEFAULT_NETWORK_CONFIG());
|
|
||||||
|
|
||||||
const instanceIdList = computed(() => {
|
|
||||||
let insts = deviceInfo.value?.running_network_instances || [];
|
|
||||||
let options = insts.map((instance: string) => {
|
|
||||||
return { uuid: instance };
|
|
||||||
});
|
|
||||||
return options;
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedInstanceId = computed({
|
|
||||||
get() {
|
|
||||||
return instanceIdList.value.find((instance) => instance.uuid === instanceId.value);
|
|
||||||
},
|
|
||||||
set(value: any) {
|
|
||||||
console.log("set instanceId", value);
|
|
||||||
router.push({ name: 'deviceManagement', params: { deviceId: deviceId.value, instanceId: value.uuid } });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const confirm = useConfirm();
|
|
||||||
const confirmDeleteNetwork = (event: any) => {
|
|
||||||
confirm.require({
|
|
||||||
target: event.currentTarget,
|
|
||||||
message: 'Do you want to delete this network?',
|
|
||||||
icon: 'pi pi-info-circle',
|
|
||||||
rejectProps: {
|
|
||||||
label: 'Cancel',
|
|
||||||
severity: 'secondary',
|
|
||||||
outlined: true
|
|
||||||
},
|
|
||||||
acceptProps: {
|
|
||||||
label: 'Delete',
|
|
||||||
severity: 'danger'
|
|
||||||
},
|
|
||||||
accept: async () => {
|
|
||||||
try {
|
|
||||||
await props.api?.delete_network(deviceId.value, instanceId.value);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
emits('update');
|
|
||||||
},
|
|
||||||
reject: () => {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// const verifyNetworkConfig = async (): Promise<ValidateConfigResponse | undefined> => {
|
|
||||||
// let ret = await props.api?.validate_config(deviceId.value, newNetworkConfig.value);
|
|
||||||
// console.log("verifyNetworkConfig", ret);
|
|
||||||
// return ret;
|
|
||||||
// }
|
|
||||||
|
|
||||||
const createNewNetwork = async () => {
|
|
||||||
try {
|
|
||||||
if (isEditing.value) {
|
|
||||||
await props.api?.delete_network(deviceId.value, instanceId.value);
|
|
||||||
}
|
|
||||||
let ret = await props.api?.run_network(deviceId.value, newNetworkConfig.value);
|
|
||||||
console.debug("createNewNetwork", ret);
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to create network, error: ' + JSON.stringify(e.response.data), life: 2000 });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
emits('update');
|
|
||||||
showCreateNetworkDialog.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newNetwork = () => {
|
|
||||||
newNetworkConfig.value = NetworkTypes.DEFAULT_NETWORK_CONFIG();
|
|
||||||
isEditing.value = false;
|
|
||||||
showCreateNetworkDialog.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const editNetwork = async () => {
|
|
||||||
if (!deviceId.value || !instanceId.value) {
|
|
||||||
toast.add({ severity: 'error', summary: 'Error', detail: 'No network instance selected', life: 2000 });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isEditing.value = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
let ret = await props.api?.get_network_config(deviceId.value, instanceId.value);
|
|
||||||
console.debug("editNetwork", ret);
|
|
||||||
newNetworkConfig.value = ret;
|
|
||||||
showCreateNetworkDialog.value = true;
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to edit network, error: ' + JSON.stringify(e.response.data), life: 2000 });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadDeviceInfo = async () => {
|
|
||||||
if (!deviceId.value || !instanceId.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let ret = await props.api?.get_network_info(deviceId.value, instanceId.value);
|
|
||||||
let device_info = ret[instanceId.value];
|
|
||||||
|
|
||||||
curNetworkInfo.value = {
|
|
||||||
instance_id: instanceId.value,
|
|
||||||
running: device_info.running,
|
|
||||||
error_msg: device_info.error_msg,
|
|
||||||
detail: device_info,
|
|
||||||
} as NetworkTypes.NetworkInstance;
|
|
||||||
}
|
|
||||||
|
|
||||||
let periodFunc = new Utils.PeriodicTask(async () => {
|
|
||||||
try {
|
|
||||||
await loadDeviceInfo();
|
|
||||||
} catch (e) {
|
|
||||||
console.debug(e);
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
periodFunc.start();
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
periodFunc.stop();
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ConfirmPopup></ConfirmPopup>
|
|
||||||
<Dialog v-model:visible="showCreateNetworkDialog" modal :header="!isEditing ? 'Create New Network' : 'Edit Network'"
|
|
||||||
:style="{ width: '55rem' }">
|
|
||||||
<Config :cur-network="newNetworkConfig" @run-network="createNewNetwork"></Config>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<Toolbar>
|
|
||||||
<template #start>
|
|
||||||
<IftaLabel>
|
|
||||||
<Select v-model="selectedInstanceId" :options="instanceIdList" optionLabel="uuid" inputId="dd-inst-id"
|
|
||||||
placeholder="Select Instance" />
|
|
||||||
<label class="mr-3" for="dd-inst-id">Network</label>
|
|
||||||
</IftaLabel>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #end>
|
|
||||||
<div class="gap-x-3 flex">
|
|
||||||
<Button @click="confirmDeleteNetwork($event)" icon="pi pi-minus" severity="danger" label="Delete"
|
|
||||||
iconPos="right" />
|
|
||||||
<Button @click="editNetwork" icon="pi pi-pen-to-square" label="Edit" iconPos="right" severity="info" />
|
|
||||||
<Button @click="newNetwork" icon="pi pi-plus" label="Create" iconPos="right" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Toolbar>
|
|
||||||
|
|
||||||
<Status v-bind:cur-network-inst="curNetworkInfo" v-if="!!selectedInstanceId">
|
|
||||||
</Status>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-4 place-content-center h-full" v-if="!selectedInstanceId">
|
|
||||||
<div class="text-center text-xl"> Select or create a network instance to manage </div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
|
@ -1,65 +1,3 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
import { Card, InputText, Password, Button, AutoComplete } from 'primevue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { useToast } from 'primevue/usetoast';
|
|
||||||
import { Api } from 'easytier-frontend-lib';
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
isRegistering: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const api = computed<Api.ApiClient>(() => new Api.ApiClient(apiHost.value));
|
|
||||||
const router = useRouter();
|
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
const username = ref('');
|
|
||||||
const password = ref('');
|
|
||||||
const registerUsername = ref('');
|
|
||||||
const registerPassword = ref('');
|
|
||||||
const captcha = ref('');
|
|
||||||
const captchaSrc = computed(() => api.value.captcha_url());
|
|
||||||
|
|
||||||
const onSubmit = async () => {
|
|
||||||
// Add your login logic here
|
|
||||||
const credential: Api.Credential = { username: username.value, password: password.value, };
|
|
||||||
let ret = await api.value?.login(credential);
|
|
||||||
if (ret.success) {
|
|
||||||
localStorage.setItem('apiHost', btoa(apiHost.value));
|
|
||||||
router.push({
|
|
||||||
name: 'dashboard',
|
|
||||||
params: { apiHost: btoa(apiHost.value) },
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
toast.add({ severity: 'error', summary: 'Login Failed', detail: ret.message, life: 2000 });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRegister = async () => {
|
|
||||||
const credential: Api.Credential = { username: registerUsername.value, password: registerPassword.value };
|
|
||||||
const registerReq: Api.RegisterData = { credentials: credential, captcha: captcha.value };
|
|
||||||
let ret = await api.value?.register(registerReq);
|
|
||||||
if (ret.success) {
|
|
||||||
toast.add({ severity: 'success', summary: 'Register Success', detail: ret.message, life: 2000 });
|
|
||||||
router.push({ name: 'login' });
|
|
||||||
} else {
|
|
||||||
toast.add({ severity: 'error', summary: 'Register Failed', detail: ret.message, life: 2000 });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultApiHost = 'http://10.147.223.128:11211'
|
|
||||||
const apiHost = ref<string>(defaultApiHost)
|
|
||||||
const apiHostSuggestions = ref<Array<string>>([])
|
|
||||||
const apiHostSearch = async (event: { query: string }) => {
|
|
||||||
apiHostSuggestions.value = [];
|
|
||||||
if (event.query) {
|
|
||||||
apiHostSuggestions.value.push(event.query);
|
|
||||||
}
|
|
||||||
apiHostSuggestions.value.push(defaultApiHost);
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center justify-center min-h-screen">
|
<div class="flex items-center justify-center min-h-screen">
|
||||||
<Card class="w-full max-w-md p-6">
|
<Card class="w-full max-w-md p-6">
|
||||||
|
@ -68,11 +6,6 @@ const apiHostSearch = async (event: { query: string }) => {
|
||||||
</h2>
|
</h2>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="p-field mb-4">
|
|
||||||
<label for="api-host" class="block text-sm font-medium">Api Host</label>
|
|
||||||
<AutoComplete id="api-host" v-model="apiHost" dropdown :suggestions="apiHostSuggestions"
|
|
||||||
@complete="apiHostSearch" class="w-full" />
|
|
||||||
</div>
|
|
||||||
<form v-if="!isRegistering" @submit.prevent="onSubmit" class="space-y-4">
|
<form v-if="!isRegistering" @submit.prevent="onSubmit" class="space-y-4">
|
||||||
<div class="p-field">
|
<div class="p-field">
|
||||||
<label for="username" class="block text-sm font-medium">Username</label>
|
<label for="username" class="block text-sm font-medium">Username</label>
|
||||||
|
@ -80,14 +13,14 @@ const apiHostSearch = async (event: { query: string }) => {
|
||||||
</div>
|
</div>
|
||||||
<div class="p-field">
|
<div class="p-field">
|
||||||
<label for="password" class="block text-sm font-medium">Password</label>
|
<label for="password" class="block text-sm font-medium">Password</label>
|
||||||
<Password id="password" v-model="password" required toggleMask :feedback="false" />
|
<Password id="password" v-model="password" required toggleMask />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<Button label="Login" type="submit" class="w-full" />
|
<Button label="Login" type="submit" class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<Button label="Register" type="button" class="w-full"
|
<Button label="Register" type="button" class="w-full" @click="isRegistering = true"
|
||||||
@click="$router.replace({ name: 'register' })" severity="secondary" />
|
severity="secondary" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
@ -99,7 +32,7 @@ const apiHostSearch = async (event: { query: string }) => {
|
||||||
<div class="p-field">
|
<div class="p-field">
|
||||||
<label for="register-password" class="block text-sm font-medium">Password</label>
|
<label for="register-password" class="block text-sm font-medium">Password</label>
|
||||||
<Password id="register-password" v-model="registerPassword" required toggleMask
|
<Password id="register-password" v-model="registerPassword" required toggleMask
|
||||||
:feedback="false" class="w-full" />
|
class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
<div class="p-field">
|
<div class="p-field">
|
||||||
<label for="captcha" class="block text-sm font-medium">Captcha</label>
|
<label for="captcha" class="block text-sm font-medium">Captcha</label>
|
||||||
|
@ -110,8 +43,8 @@ const apiHostSearch = async (event: { query: string }) => {
|
||||||
<Button label="Register" type="submit" class="w-full" />
|
<Button label="Register" type="submit" class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<Button label="Back to Login" type="button" class="w-full"
|
<Button label="Back to Login" type="button" class="w-full" @click="isRegistering = false"
|
||||||
@click="$router.replace({ name: 'login' })" severity="secondary" />
|
severity="secondary" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
@ -119,4 +52,42 @@ const apiHostSearch = async (event: { query: string }) => {
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { Card, InputText, Password, Button } from 'primevue';
|
||||||
|
import ApiClient from '../modules/api';
|
||||||
|
import { Credential } from '../modules/api';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
api: ApiClient,
|
||||||
|
});
|
||||||
|
|
||||||
|
const api = props.api;
|
||||||
|
|
||||||
|
const username = ref('');
|
||||||
|
const password = ref('');
|
||||||
|
const registerUsername = ref('');
|
||||||
|
const registerPassword = ref('');
|
||||||
|
const captcha = ref('');
|
||||||
|
const captchaSrc = computed(() => api?.captcha_url());
|
||||||
|
const isRegistering = ref(false);
|
||||||
|
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
console.log('Username:', username.value);
|
||||||
|
console.log('Password:', password.value);
|
||||||
|
// Add your login logic here
|
||||||
|
const credential: Credential = { username: username.value, password: password.value, };
|
||||||
|
const ret = await api?.login(credential);
|
||||||
|
alert(ret?.message);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRegister = () => {
|
||||||
|
console.log('Register Username:', registerUsername.value);
|
||||||
|
console.log('Register Password:', registerPassword.value);
|
||||||
|
console.log('Captcha:', captcha.value);
|
||||||
|
// Add your register logic here
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
|
@ -1,173 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { Api, I18nUtils } from 'easytier-frontend-lib'
|
|
||||||
import { computed, onMounted, ref } from 'vue';
|
|
||||||
import { Button, TieredMenu } from 'primevue';
|
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
|
||||||
import { useDialog } from 'primevue/usedialog';
|
|
||||||
import ChangePassword from './ChangePassword.vue';
|
|
||||||
import Icon from '../assets/easytier.png'
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
const router = useRouter();
|
|
||||||
const api = computed<Api.ApiClient | undefined>(() => {
|
|
||||||
try {
|
|
||||||
return new Api.ApiClient(atob(route.params.apiHost as string), () => {
|
|
||||||
router.push({ name: 'login' });
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
router.push({ name: 'login' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const dialog = useDialog();
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await I18nUtils.loadLanguageAsync('cn')
|
|
||||||
});
|
|
||||||
|
|
||||||
const userMenu = ref();
|
|
||||||
const userMenuItems = ref([
|
|
||||||
{
|
|
||||||
label: 'Change Password',
|
|
||||||
icon: 'pi pi-key',
|
|
||||||
command: () => {
|
|
||||||
console.log('File');
|
|
||||||
let ret = dialog.open(ChangePassword, {
|
|
||||||
props: {
|
|
||||||
modal: true,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
api: api.value,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("return", ret)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Logout',
|
|
||||||
icon: 'pi pi-sign-out',
|
|
||||||
command: async () => {
|
|
||||||
try {
|
|
||||||
await api.value?.logout();
|
|
||||||
} catch (e) {
|
|
||||||
console.error("logout failed", e);
|
|
||||||
}
|
|
||||||
router.push({ name: 'login' });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
const forceShowSideBar = ref(false)
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- https://flowbite.com/docs/components/sidebar/#sidebar-with-navbar -->
|
|
||||||
<template>
|
|
||||||
<nav class="fixed top-0 z-50 w-full bg-white border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700">
|
|
||||||
<div class="px-3 py-3 lg:px-5 lg:pl-3">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex items-center justify-start rtl:justify-end">
|
|
||||||
<div class="sm:hidden">
|
|
||||||
<Button type="button" aria-haspopup="true" icon="pi pi-list" variant="text" size="large"
|
|
||||||
severity="contrast" @click="forceShowSideBar = !forceShowSideBar" />
|
|
||||||
</div>
|
|
||||||
<a href="https://easytier.top" class="flex ms-2 md:me-24">
|
|
||||||
<img :src="Icon" class="h-9 me-3" alt="FlowBite Logo" />
|
|
||||||
<span
|
|
||||||
class="self-center text-xl font-semibold sm:text-2xl whitespace-nowrap dark:text-white">EasyTier</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="flex items-center ms-3">
|
|
||||||
<div>
|
|
||||||
<Button type="button" @click="userMenu.toggle($event)" aria-haspopup="true"
|
|
||||||
aria-controls="user-menu" icon="pi pi-user" raised rounded />
|
|
||||||
<TieredMenu ref="userMenu" id="user-menu" :model="userMenuItems" popup />
|
|
||||||
</div>
|
|
||||||
<div class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded shadow dark:bg-gray-700 dark:divide-gray-600"
|
|
||||||
id="dropdown-user">
|
|
||||||
<div class="px-4 py-3" role="none">
|
|
||||||
<p class="text-sm text-gray-900 dark:text-white" role="none">
|
|
||||||
Neil Sims
|
|
||||||
</p>
|
|
||||||
<p class="text-sm font-medium text-gray-900 truncate dark:text-gray-300" role="none">
|
|
||||||
neil.sims@flowbite.com
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<ul class="py-1" role="none">
|
|
||||||
<li>
|
|
||||||
<a href="#"
|
|
||||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
|
||||||
role="menuitem">Dashboard</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#"
|
|
||||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
|
||||||
role="menuitem">Settings</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#"
|
|
||||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
|
||||||
role="menuitem">Earnings</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#"
|
|
||||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
|
||||||
role="menuitem">Sign out</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<aside id="logo-sidebar"
|
|
||||||
class="fixed top-1 left-0 z-40 w-64 h-screen pt-20 transition-transform bg-white border-r border-gray-201 sm:translate-x-0 dark:bg-gray-800 dark:border-gray-700"
|
|
||||||
:class="{ '-translate-x-full': !forceShowSideBar }" aria-label="Sidebar">
|
|
||||||
<div class="h-full px-3 pb-4 overflow-y-auto bg-white dark:bg-gray-800">
|
|
||||||
<ul class="space-y-2 font-medium">
|
|
||||||
<li>
|
|
||||||
<Button variant="text" class="w-full justify-start gap-x-3 pl-1.5 sidebar-button"
|
|
||||||
severity="contrast" @click="router.push({ name: 'dashboard' })">
|
|
||||||
<i class="pi pi-chart-pie text-xl"></i>
|
|
||||||
<span class="mb-0.5">DashBoard</span>
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Button variant="text" class="w-full justify-start gap-x-3 pl-1.5 sidebar-button"
|
|
||||||
severity="contrast" @click="router.push({ name: 'deviceList' })">
|
|
||||||
<i class="pi pi-server text-xl"></i>
|
|
||||||
<span class="mb-0.5">Devices</span>
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Button variant="text" class="w-full justify-start gap-x-3 pl-1.5 sidebar-button"
|
|
||||||
severity="contrast" @click="router.push({ name: 'login' })">
|
|
||||||
<i class="pi pi-sign-in text-xl"></i>
|
|
||||||
<span class="mb-0.5">Login Page</span>
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<div class="p-4 sm:ml-64">
|
|
||||||
<div class="p-4 border-2 border-gray-200 border-dashed rounded-lg dark:border-gray-700 mt-14">
|
|
||||||
<div class="grid grid-cols-1 gap-4">
|
|
||||||
<RouterView v-slot="{ Component }">
|
|
||||||
<component :is="Component" :api="api" />
|
|
||||||
</RouterView>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.sidebar-button {
|
|
||||||
text-align: left;
|
|
||||||
justify-content: left;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -7,72 +7,6 @@ import PrimeVue from 'primevue/config'
|
||||||
import Aura from '@primevue/themes/aura'
|
import Aura from '@primevue/themes/aura'
|
||||||
import ConfirmationService from 'primevue/confirmationservice';
|
import ConfirmationService from 'primevue/confirmationservice';
|
||||||
|
|
||||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
|
||||||
import MainPage from './components/MainPage.vue'
|
|
||||||
import Login from './components/Login.vue'
|
|
||||||
import DeviceList from './components/DeviceList.vue'
|
|
||||||
import DeviceManagement from './components/DeviceManagement.vue'
|
|
||||||
import Dashboard from './components/Dashboard.vue'
|
|
||||||
import DialogService from 'primevue/dialogservice';
|
|
||||||
import ToastService from 'primevue/toastservice';
|
|
||||||
|
|
||||||
const routes = [
|
|
||||||
{
|
|
||||||
path: '/auth', children: [
|
|
||||||
{
|
|
||||||
name: 'login',
|
|
||||||
path: '',
|
|
||||||
component: Login,
|
|
||||||
alias: 'login',
|
|
||||||
props: { isRegistering: false }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'register',
|
|
||||||
path: 'register',
|
|
||||||
component: Login,
|
|
||||||
props: { isRegistering: true }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/h/:apiHost', component: MainPage, children: [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
alias: 'dashboard',
|
|
||||||
name: 'dashboard',
|
|
||||||
component: Dashboard,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'deviceList',
|
|
||||||
name: 'deviceList',
|
|
||||||
component: DeviceList,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'device/:deviceId/:instanceId?',
|
|
||||||
name: 'deviceManagement',
|
|
||||||
component: DeviceManagement,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/:pathMatch(.*)*', name: 'notFound', redirect: () => {
|
|
||||||
let apiHost = localStorage.getItem('apiHost');
|
|
||||||
if (apiHost) {
|
|
||||||
return { name: 'dashboard', params: { apiHost: apiHost } }
|
|
||||||
} else {
|
|
||||||
return { name: 'login' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const router = createRouter({
|
|
||||||
history: createWebHashHistory(),
|
|
||||||
routes,
|
|
||||||
})
|
|
||||||
|
|
||||||
createApp(App).use(PrimeVue,
|
createApp(App).use(PrimeVue,
|
||||||
{
|
{
|
||||||
theme: {
|
theme: {
|
||||||
|
@ -87,4 +21,4 @@ createApp(App).use(PrimeVue,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
).use(ToastService as any).use(DialogService as any).use(router).use(ConfirmationService as any).use(EasytierFrontendLib).mount('#app')
|
).use(ConfirmationService as any).use(EasytierFrontendLib).mount('#app')
|
||||||
|
|
|
@ -11,8 +11,8 @@ export interface LoginResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegisterResponse {
|
export interface RegisterResponse {
|
||||||
success: boolean;
|
|
||||||
message: string;
|
message: string;
|
||||||
|
user: any; // 同上
|
||||||
}
|
}
|
||||||
|
|
||||||
// 定义请求体数据结构
|
// 定义请求体数据结构
|
||||||
|
@ -22,27 +22,21 @@ export interface Credential {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegisterData {
|
export interface RegisterData {
|
||||||
credentials: Credential;
|
credential: Credential;
|
||||||
captcha: string;
|
captcha: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Summary {
|
class ApiClient {
|
||||||
device_count: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ApiClient {
|
|
||||||
private client: AxiosInstance;
|
private client: AxiosInstance;
|
||||||
private authFailedCb: Function | undefined;
|
|
||||||
|
|
||||||
constructor(baseUrl: string, authFailedCb: Function | undefined = undefined) {
|
constructor(baseUrl: string) {
|
||||||
this.client = axios.create({
|
this.client = axios.create({
|
||||||
baseURL: baseUrl + '/api/v1',
|
baseURL: baseUrl,
|
||||||
withCredentials: true, // 如果需要支持跨域携带cookie
|
withCredentials: true, // 如果需要支持跨域携带cookie
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.authFailedCb = authFailedCb;
|
|
||||||
|
|
||||||
// 添加请求拦截器
|
// 添加请求拦截器
|
||||||
this.client.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
this.client.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
||||||
|
@ -53,18 +47,12 @@ export class ApiClient {
|
||||||
|
|
||||||
// 添加响应拦截器
|
// 添加响应拦截器
|
||||||
this.client.interceptors.response.use((response: AxiosResponse) => {
|
this.client.interceptors.response.use((response: AxiosResponse) => {
|
||||||
console.debug('Axios Response:', response);
|
console.log('Axios Response:', response);
|
||||||
return response.data; // 假设服务器返回的数据都在data属性中
|
return response.data; // 假设服务器返回的数据都在data属性中
|
||||||
}, (error: any) => {
|
}, (error: any) => {
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
let response: AxiosResponse = error.response;
|
|
||||||
if (response.status == 401 && this.authFailedCb) {
|
|
||||||
console.error('Unauthorized:', response.data);
|
|
||||||
this.authFailedCb();
|
|
||||||
} else {
|
|
||||||
// 请求已发出,但是服务器响应的状态码不在2xx范围
|
// 请求已发出,但是服务器响应的状态码不在2xx范围
|
||||||
console.error('Response Error:', error.response.data);
|
console.error('Response Error:', error.response.data);
|
||||||
}
|
|
||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
// 请求已发出,但是没有收到响应
|
// 请求已发出,但是没有收到响应
|
||||||
console.error('Request Error:', error.request);
|
console.error('Request Error:', error.request);
|
||||||
|
@ -76,20 +64,6 @@ export class ApiClient {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注册
|
|
||||||
public async register(data: RegisterData): Promise<RegisterResponse> {
|
|
||||||
try {
|
|
||||||
const response = await this.client.post<RegisterResponse>('/auth/register', data);
|
|
||||||
console.log("register response:", response);
|
|
||||||
return { success: true, message: 'Register success', };
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof AxiosError) {
|
|
||||||
return { success: false, message: 'Failed to register, error: ' + JSON.stringify(error.response?.data), };
|
|
||||||
}
|
|
||||||
return { success: false, message: 'Unknown error, error: ' + error, };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 登录
|
// 登录
|
||||||
public async login(data: Credential): Promise<LoginResponse> {
|
public async login(data: Credential): Promise<LoginResponse> {
|
||||||
try {
|
try {
|
||||||
|
@ -108,24 +82,10 @@ export class ApiClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async logout() {
|
// 注册
|
||||||
await this.client.get('/auth/logout');
|
public async register(data: RegisterData): Promise<RegisterResponse> {
|
||||||
if (this.authFailedCb) {
|
const response = await this.client.post<RegisterResponse>('/auth/register', data);
|
||||||
this.authFailedCb();
|
return response.data;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async change_password(new_password: string) {
|
|
||||||
await this.client.put('/auth/password', { new_password: new_password });
|
|
||||||
}
|
|
||||||
|
|
||||||
public async check_login_status() {
|
|
||||||
try {
|
|
||||||
await this.client.get('/auth/check_login_status');
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async list_session() {
|
public async list_session() {
|
||||||
|
@ -143,11 +103,6 @@ export class ApiClient {
|
||||||
return response.info.map;
|
return response.info.map;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async get_network_config(machine_id: string, inst_id: string): Promise<any> {
|
|
||||||
const response = await this.client.get<any, Record<string, any>>('/machines/' + machine_id + '/networks/config/' + inst_id);
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async validate_config(machine_id: string, config: any): Promise<ValidateConfigResponse> {
|
public async validate_config(machine_id: string, config: any): Promise<ValidateConfigResponse> {
|
||||||
const response = await this.client.post<any, ValidateConfigResponse>(`/machines/${machine_id}/validate-config`, {
|
const response = await this.client.post<any, ValidateConfigResponse>(`/machines/${machine_id}/validate-config`, {
|
||||||
config: config,
|
config: config,
|
||||||
|
@ -155,7 +110,7 @@ export class ApiClient {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async run_network(machine_id: string, config: any): Promise<undefined> {
|
public async run_network(machine_id: string, config: string): Promise<undefined> {
|
||||||
await this.client.post<string>(`/machines/${machine_id}/networks`, {
|
await this.client.post<string>(`/machines/${machine_id}/networks`, {
|
||||||
config: config,
|
config: config,
|
||||||
});
|
});
|
||||||
|
@ -165,13 +120,8 @@ export class ApiClient {
|
||||||
await this.client.delete<string>(`/machines/${machine_id}/networks/${inst_id}`);
|
await this.client.delete<string>(`/machines/${machine_id}/networks/${inst_id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async get_summary(): Promise<Summary> {
|
|
||||||
const response = await this.client.get<any, Summary>('/summary');
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
public captcha_url() {
|
public captcha_url() {
|
||||||
return this.client.defaults.baseURL + '/auth/captcha';
|
return this.client.defaults.baseURL + 'auth/captcha';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,5 +22,5 @@
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "../frontend-lib/src/modules/api.ts"]
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,8 @@ use easytier::{
|
||||||
rpc_impl::bidirect::BidirectRpcManager,
|
rpc_impl::bidirect::BidirectRpcManager,
|
||||||
rpc_types::{self, controller::BaseController},
|
rpc_types::{self, controller::BaseController},
|
||||||
web::{
|
web::{
|
||||||
HeartbeatRequest, HeartbeatResponse, NetworkConfig, RunNetworkInstanceRequest,
|
HeartbeatRequest, HeartbeatResponse, RunNetworkInstanceRequest, WebClientService,
|
||||||
WebClientService, WebClientServiceClientFactory, WebServerService,
|
WebClientServiceClientFactory, WebServerService, WebServerServiceServer,
|
||||||
WebServerServiceServer,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
tunnel::Tunnel,
|
tunnel::Tunnel,
|
||||||
|
@ -161,13 +160,7 @@ impl Session {
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let req = req.unwrap();
|
let req = req.unwrap();
|
||||||
if req.machine_id.is_none() {
|
|
||||||
tracing::warn!(?req, "Machine id is not set, ignore");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let running_inst_ids = req
|
let running_inst_ids = req
|
||||||
.running_network_instances
|
.running_network_instances
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -194,11 +187,7 @@ impl Session {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let local_configs = match storage
|
let local_configs = match storage.db.list_network_configs(user_id, true).await {
|
||||||
.db
|
|
||||||
.list_network_configs(user_id, Some(req.machine_id.unwrap().into()), true)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(configs) => configs,
|
Ok(configs) => configs,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to list network configs, error: {:?}", e);
|
tracing::error!("Failed to list network configs, error: {:?}", e);
|
||||||
|
@ -217,9 +206,7 @@ impl Session {
|
||||||
BaseController::default(),
|
BaseController::default(),
|
||||||
RunNetworkInstanceRequest {
|
RunNetworkInstanceRequest {
|
||||||
inst_id: Some(c.network_instance_id.clone().into()),
|
inst_id: Some(c.network_instance_id.clone().into()),
|
||||||
config: Some(
|
config: c.network_config,
|
||||||
serde_json::from_str::<NetworkConfig>(&c.network_config).unwrap(),
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
|
@ -16,7 +16,7 @@ pub struct StorageToken {
|
||||||
pub struct StorageInner {
|
pub struct StorageInner {
|
||||||
// some map for indexing
|
// some map for indexing
|
||||||
pub token_clients_map: DashMap<String, DashSet<url::Url>>,
|
pub token_clients_map: DashMap<String, DashSet<url::Url>>,
|
||||||
pub machine_client_url_map: DashMap<uuid::Uuid, DashSet<url::Url>>,
|
pub machine_client_url_map: DashMap<uuid::Uuid, url::Url>,
|
||||||
pub db: Db,
|
pub db: Db,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,9 +51,7 @@ impl Storage {
|
||||||
|
|
||||||
self.0
|
self.0
|
||||||
.machine_client_url_map
|
.machine_client_url_map
|
||||||
.entry(stoken.machine_id)
|
.insert(stoken.machine_id, stoken.client_url.clone());
|
||||||
.or_insert_with(DashSet::new)
|
|
||||||
.insert(stoken.client_url.clone());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_client(&self, stoken: &StorageToken) {
|
pub fn remove_client(&self, stoken: &StorageToken) {
|
||||||
|
@ -62,12 +60,7 @@ impl Storage {
|
||||||
set.is_empty()
|
set.is_empty()
|
||||||
});
|
});
|
||||||
|
|
||||||
self.0
|
self.0.machine_client_url_map.remove(&stoken.machine_id);
|
||||||
.machine_client_url_map
|
|
||||||
.remove_if(&stoken.machine_id, |_, set| {
|
|
||||||
set.remove(&stoken.client_url);
|
|
||||||
set.is_empty()
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn weak_ref(&self) -> WeakRefStorage {
|
pub fn weak_ref(&self) -> WeakRefStorage {
|
||||||
|
@ -78,8 +71,7 @@ impl Storage {
|
||||||
self.0
|
self.0
|
||||||
.machine_client_url_map
|
.machine_client_url_map
|
||||||
.get(&machine_id)
|
.get(&machine_id)
|
||||||
.map(|url| url.iter().next().map(|url| url.clone()))
|
.map(|url| url.clone())
|
||||||
.flatten()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_token_clients(&self, token: &str) -> Vec<url::Url> {
|
pub fn list_token_clients(&self, token: &str) -> Vec<url::Url> {
|
||||||
|
|
|
@ -9,8 +9,6 @@ pub struct Model {
|
||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub user_id: i32,
|
pub user_id: i32,
|
||||||
#[sea_orm(column_type = "Text")]
|
|
||||||
pub device_id: String,
|
|
||||||
#[sea_orm(column_type = "Text", unique)]
|
#[sea_orm(column_type = "Text", unique)]
|
||||||
pub network_instance_id: String,
|
pub network_instance_id: String,
|
||||||
#[sea_orm(column_type = "Text")]
|
#[sea_orm(column_type = "Text")]
|
||||||
|
|
|
@ -65,7 +65,6 @@ impl Db {
|
||||||
pub async fn insert_or_update_user_network_config<T: ToString>(
|
pub async fn insert_or_update_user_network_config<T: ToString>(
|
||||||
&self,
|
&self,
|
||||||
user_id: UserIdInDb,
|
user_id: UserIdInDb,
|
||||||
device_id: uuid::Uuid,
|
|
||||||
network_inst_id: uuid::Uuid,
|
network_inst_id: uuid::Uuid,
|
||||||
network_config: T,
|
network_config: T,
|
||||||
) -> Result<(), DbErr> {
|
) -> Result<(), DbErr> {
|
||||||
|
@ -82,7 +81,6 @@ impl Db {
|
||||||
.to_owned();
|
.to_owned();
|
||||||
let insert_m = urnc::ActiveModel {
|
let insert_m = urnc::ActiveModel {
|
||||||
user_id: sea_orm::Set(user_id),
|
user_id: sea_orm::Set(user_id),
|
||||||
device_id: sea_orm::Set(device_id.to_string()),
|
|
||||||
network_instance_id: sea_orm::Set(network_inst_id.to_string()),
|
network_instance_id: sea_orm::Set(network_inst_id.to_string()),
|
||||||
network_config: sea_orm::Set(network_config.to_string()),
|
network_config: sea_orm::Set(network_config.to_string()),
|
||||||
disabled: sea_orm::Set(false),
|
disabled: sea_orm::Set(false),
|
||||||
|
@ -118,7 +116,6 @@ impl Db {
|
||||||
pub async fn list_network_configs(
|
pub async fn list_network_configs(
|
||||||
&self,
|
&self,
|
||||||
user_id: UserIdInDb,
|
user_id: UserIdInDb,
|
||||||
device_id: Option<uuid::Uuid>,
|
|
||||||
only_enabled: bool,
|
only_enabled: bool,
|
||||||
) -> Result<Vec<user_running_network_configs::Model>, DbErr> {
|
) -> Result<Vec<user_running_network_configs::Model>, DbErr> {
|
||||||
use entity::user_running_network_configs as urnc;
|
use entity::user_running_network_configs as urnc;
|
||||||
|
@ -129,11 +126,6 @@ impl Db {
|
||||||
} else {
|
} else {
|
||||||
configs
|
configs
|
||||||
};
|
};
|
||||||
let configs = if let Some(device_id) = device_id {
|
|
||||||
configs.filter(urnc::Column::DeviceId.eq(device_id.to_string()))
|
|
||||||
} else {
|
|
||||||
configs
|
|
||||||
};
|
|
||||||
|
|
||||||
let configs = configs.all(self.orm_db()).await?;
|
let configs = configs.all(self.orm_db()).await?;
|
||||||
|
|
||||||
|
@ -175,9 +167,8 @@ mod tests {
|
||||||
let user_id = 1;
|
let user_id = 1;
|
||||||
let network_config = "test_config";
|
let network_config = "test_config";
|
||||||
let inst_id = uuid::Uuid::new_v4();
|
let inst_id = uuid::Uuid::new_v4();
|
||||||
let device_id = uuid::Uuid::new_v4();
|
|
||||||
|
|
||||||
db.insert_or_update_user_network_config(user_id, device_id, inst_id, network_config)
|
db.insert_or_update_user_network_config(user_id, inst_id, network_config)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -192,7 +183,7 @@ mod tests {
|
||||||
|
|
||||||
// overwrite the config
|
// overwrite the config
|
||||||
let network_config = "test_config2";
|
let network_config = "test_config2";
|
||||||
db.insert_or_update_user_network_config(user_id, device_id, inst_id, network_config)
|
db.insert_or_update_user_network_config(user_id, inst_id, network_config)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -202,17 +193,14 @@ mod tests {
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
println!("device: {}, {:?}", device_id, result2);
|
println!("{:?}", result2);
|
||||||
assert_eq!(result2.network_config, network_config);
|
assert_eq!(result2.network_config, network_config);
|
||||||
|
|
||||||
assert_eq!(result.create_time, result2.create_time);
|
assert_eq!(result.create_time, result2.create_time);
|
||||||
assert_ne!(result.update_time, result2.update_time);
|
assert_ne!(result.update_time, result2.update_time);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
db.list_network_configs(user_id, Some(device_id), true)
|
db.list_network_configs(user_id, true).await.unwrap().len(),
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.len(),
|
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,94 @@ use sea_orm_migration::{prelude::*, schema::*};
|
||||||
|
|
||||||
pub struct Migration;
|
pub struct Migration;
|
||||||
|
|
||||||
|
/*
|
||||||
|
-- # Entity schema.
|
||||||
|
|
||||||
|
-- Create `users` table.
|
||||||
|
create table if not exists users (
|
||||||
|
id integer primary key autoincrement,
|
||||||
|
username text not null unique,
|
||||||
|
password text not null
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create `groups` table.
|
||||||
|
create table if not exists groups (
|
||||||
|
id integer primary key autoincrement,
|
||||||
|
name text not null unique
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create `permissions` table.
|
||||||
|
create table if not exists permissions (
|
||||||
|
id integer primary key autoincrement,
|
||||||
|
name text not null unique
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
-- # Join tables.
|
||||||
|
|
||||||
|
-- Create `users_groups` table for many-to-many relationships between users and groups.
|
||||||
|
create table if not exists users_groups (
|
||||||
|
user_id integer references users(id),
|
||||||
|
group_id integer references groups(id),
|
||||||
|
primary key (user_id, group_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create `groups_permissions` table for many-to-many relationships between groups and permissions.
|
||||||
|
create table if not exists groups_permissions (
|
||||||
|
group_id integer references groups(id),
|
||||||
|
permission_id integer references permissions(id),
|
||||||
|
primary key (group_id, permission_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
-- # Fixture hydration.
|
||||||
|
|
||||||
|
-- Insert "user" user. password: "user"
|
||||||
|
insert into users (username, password)
|
||||||
|
values (
|
||||||
|
'user',
|
||||||
|
'$argon2i$v=19$m=16,t=2,p=1$dHJ5dXZkYmZkYXM$UkrNqWz0BbSVBq4ykLSuJw'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Insert "admin" user. password: "admin"
|
||||||
|
insert into users (username, password)
|
||||||
|
values (
|
||||||
|
'admin',
|
||||||
|
'$argon2i$v=19$m=16,t=2,p=1$Ymd1Y2FlcnQ$x0q4oZinW9S1ZB9BcaHEpQ'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Insert "users" and "superusers" groups.
|
||||||
|
insert into groups (name) values ('users');
|
||||||
|
insert into groups (name) values ('superusers');
|
||||||
|
|
||||||
|
-- Insert individual permissions.
|
||||||
|
insert into permissions (name) values ('sessions');
|
||||||
|
insert into permissions (name) values ('devices');
|
||||||
|
|
||||||
|
-- Insert group permissions.
|
||||||
|
insert into groups_permissions (group_id, permission_id)
|
||||||
|
values (
|
||||||
|
(select id from groups where name = 'users'),
|
||||||
|
(select id from permissions where name = 'devices')
|
||||||
|
), (
|
||||||
|
(select id from groups where name = 'superusers'),
|
||||||
|
(select id from permissions where name = 'sessions')
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Insert users into groups.
|
||||||
|
insert into users_groups (user_id, group_id)
|
||||||
|
values (
|
||||||
|
(select id from users where username = 'user'),
|
||||||
|
(select id from groups where name = 'users')
|
||||||
|
), (
|
||||||
|
(select id from users where username = 'admin'),
|
||||||
|
(select id from groups where name = 'users')
|
||||||
|
), (
|
||||||
|
(select id from users where username = 'admin'),
|
||||||
|
(select id from groups where name = 'superusers')
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
|
||||||
impl MigrationName for Migration {
|
impl MigrationName for Migration {
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
"m20241029_000001_init"
|
"m20241029_000001_init"
|
||||||
|
@ -53,7 +141,6 @@ enum UserRunningNetworkConfigs {
|
||||||
Table,
|
Table,
|
||||||
Id,
|
Id,
|
||||||
UserId,
|
UserId,
|
||||||
DeviceId,
|
|
||||||
NetworkInstanceId,
|
NetworkInstanceId,
|
||||||
NetworkConfig,
|
NetworkConfig,
|
||||||
Disabled,
|
Disabled,
|
||||||
|
@ -186,7 +273,6 @@ impl MigrationTrait for Migration {
|
||||||
.table(UserRunningNetworkConfigs::Table)
|
.table(UserRunningNetworkConfigs::Table)
|
||||||
.col(pk_auto(UserRunningNetworkConfigs::Id).not_null())
|
.col(pk_auto(UserRunningNetworkConfigs::Id).not_null())
|
||||||
.col(integer(UserRunningNetworkConfigs::UserId).not_null())
|
.col(integer(UserRunningNetworkConfigs::UserId).not_null())
|
||||||
.col(text(UserRunningNetworkConfigs::DeviceId).not_null())
|
|
||||||
.col(
|
.col(
|
||||||
text(UserRunningNetworkConfigs::NetworkInstanceId)
|
text(UserRunningNetworkConfigs::NetworkInstanceId)
|
||||||
.unique_key()
|
.unique_key()
|
||||||
|
|
|
@ -22,10 +22,6 @@ pub struct LoginResult {
|
||||||
pub fn router() -> Router<AppStateInner> {
|
pub fn router() -> Router<AppStateInner> {
|
||||||
let r = Router::new()
|
let r = Router::new()
|
||||||
.route("/api/v1/auth/password", put(self::put::change_password))
|
.route("/api/v1/auth/password", put(self::put::change_password))
|
||||||
.route(
|
|
||||||
"/api/v1/auth/check_login_status",
|
|
||||||
get(self::get::check_login_status),
|
|
||||||
)
|
|
||||||
.route_layer(login_required!(Backend));
|
.route_layer(login_required!(Backend));
|
||||||
Router::new()
|
Router::new()
|
||||||
.merge(r)
|
.merge(r)
|
||||||
|
@ -172,17 +168,4 @@ mod get {
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn check_login_status(
|
|
||||||
auth_session: AuthSession,
|
|
||||||
) -> Result<Json<Void>, HttpHandleError> {
|
|
||||||
if auth_session.user.is_some() {
|
|
||||||
Ok(Json(Void::default()))
|
|
||||||
} else {
|
|
||||||
Err((
|
|
||||||
StatusCode::UNAUTHORIZED,
|
|
||||||
Json::from(other_error("Not logged in")),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ use axum_login::tower_sessions::{ExpiredDeletion, SessionManagerLayer};
|
||||||
use axum_login::{login_required, AuthManagerLayerBuilder, AuthzBackend};
|
use axum_login::{login_required, AuthManagerLayerBuilder, AuthzBackend};
|
||||||
use axum_messages::MessagesManagerLayer;
|
use axum_messages::MessagesManagerLayer;
|
||||||
use easytier::common::scoped_task::ScopedTask;
|
use easytier::common::scoped_task::ScopedTask;
|
||||||
use easytier::proto::rpc_types;
|
use easytier::proto::{rpc_types};
|
||||||
use network::NetworkApi;
|
use network::NetworkApi;
|
||||||
use sea_orm::DbErr;
|
use sea_orm::DbErr;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
@ -43,11 +43,6 @@ type AppState = State<AppStateInner>;
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
struct ListSessionJsonResp(Vec<StorageToken>);
|
struct ListSessionJsonResp(Vec<StorageToken>);
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
|
||||||
struct GetSummaryJsonResp {
|
|
||||||
device_count: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
pub struct Error {
|
pub struct Error {
|
||||||
message: String,
|
message: String,
|
||||||
|
@ -103,32 +98,16 @@ impl RestfulServer {
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
State(client_mgr): AppState,
|
State(client_mgr): AppState,
|
||||||
) -> Result<Json<ListSessionJsonResp>, HttpHandleError> {
|
) -> Result<Json<ListSessionJsonResp>, HttpHandleError> {
|
||||||
let perms = auth_session
|
let pers = auth_session
|
||||||
.backend
|
.backend
|
||||||
.get_group_permissions(auth_session.user.as_ref().unwrap())
|
.get_group_permissions(auth_session.user.as_ref().unwrap())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
println!("{:?}", perms);
|
println!("{:?}", pers);
|
||||||
let ret = client_mgr.list_sessions().await;
|
let ret = client_mgr.list_sessions().await;
|
||||||
Ok(ListSessionJsonResp(ret).into())
|
Ok(ListSessionJsonResp(ret).into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_get_summary(
|
|
||||||
auth_session: AuthSession,
|
|
||||||
State(client_mgr): AppState,
|
|
||||||
) -> Result<Json<GetSummaryJsonResp>, HttpHandleError> {
|
|
||||||
let Some(user) = auth_session.user else {
|
|
||||||
return Err((StatusCode::UNAUTHORIZED, other_error("No such user").into()));
|
|
||||||
};
|
|
||||||
|
|
||||||
let machines = client_mgr.list_machine_by_token(user.tokens[0].clone());
|
|
||||||
|
|
||||||
Ok(GetSummaryJsonResp {
|
|
||||||
device_count: machines.len() as u32,
|
|
||||||
}
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn start(&mut self) -> Result<(), anyhow::Error> {
|
pub async fn start(&mut self) -> Result<(), anyhow::Error> {
|
||||||
let listener = TcpListener::bind(self.bind_addr).await?;
|
let listener = TcpListener::bind(self.bind_addr).await?;
|
||||||
|
|
||||||
|
@ -164,7 +143,6 @@ impl RestfulServer {
|
||||||
let auth_layer = AuthManagerLayerBuilder::new(backend, session_layer).build();
|
let auth_layer = AuthManagerLayerBuilder::new(backend, session_layer).build();
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/api/v1/summary", get(Self::handle_get_summary))
|
|
||||||
.route("/api/v1/sessions", get(Self::handle_list_all_sessions))
|
.route("/api/v1/sessions", get(Self::handle_list_all_sessions))
|
||||||
.merge(self.network_api.build_route())
|
.merge(self.network_api.build_route())
|
||||||
.route_layer(login_required!(Backend))
|
.route_layer(login_required!(Backend))
|
||||||
|
|
|
@ -9,7 +9,7 @@ use dashmap::DashSet;
|
||||||
use easytier::launcher::NetworkConfig;
|
use easytier::launcher::NetworkConfig;
|
||||||
use easytier::proto::common::Void;
|
use easytier::proto::common::Void;
|
||||||
use easytier::proto::rpc_types::controller::BaseController;
|
use easytier::proto::rpc_types::controller::BaseController;
|
||||||
use easytier::proto::web::*;
|
use easytier::proto::{web::*};
|
||||||
|
|
||||||
use crate::client_manager::session::Session;
|
use crate::client_manager::session::Session;
|
||||||
use crate::client_manager::ClientManager;
|
use crate::client_manager::ClientManager;
|
||||||
|
@ -38,7 +38,7 @@ struct ValidateConfigJsonReq {
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
struct RunNetworkJsonReq {
|
struct RunNetworkJsonReq {
|
||||||
config: NetworkConfig,
|
config: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
@ -145,7 +145,7 @@ impl NetworkApi {
|
||||||
BaseController::default(),
|
BaseController::default(),
|
||||||
RunNetworkInstanceRequest {
|
RunNetworkInstanceRequest {
|
||||||
inst_id: None,
|
inst_id: None,
|
||||||
config: Some(config.clone()),
|
config: config.clone(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
@ -155,9 +155,8 @@ impl NetworkApi {
|
||||||
.db()
|
.db()
|
||||||
.insert_or_update_user_network_config(
|
.insert_or_update_user_network_config(
|
||||||
auth_session.user.as_ref().unwrap().id(),
|
auth_session.user.as_ref().unwrap().id(),
|
||||||
machine_id,
|
|
||||||
resp.inst_id.clone().unwrap_or_default().into(),
|
resp.inst_id.clone().unwrap_or_default().into(),
|
||||||
serde_json::to_string(&config).unwrap(),
|
config,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(convert_db_error)?;
|
.map_err(convert_db_error)?;
|
||||||
|
@ -289,36 +288,6 @@ impl NetworkApi {
|
||||||
Ok(Json(ListMachineJsonResp { machines }))
|
Ok(Json(ListMachineJsonResp { machines }))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_get_network_config(
|
|
||||||
auth_session: AuthSession,
|
|
||||||
State(client_mgr): AppState,
|
|
||||||
Path((machine_id, inst_id)): Path<(uuid::Uuid, uuid::Uuid)>,
|
|
||||||
) -> Result<Json<NetworkConfig>, HttpHandleError> {
|
|
||||||
let inst_id = inst_id.to_string();
|
|
||||||
|
|
||||||
let db_row = client_mgr
|
|
||||||
.db()
|
|
||||||
.list_network_configs(auth_session.user.unwrap().id(), Some(machine_id), false)
|
|
||||||
.await
|
|
||||||
.map_err(convert_db_error)?
|
|
||||||
.iter()
|
|
||||||
.find(|x| x.network_instance_id == inst_id)
|
|
||||||
.map(|x| x.network_config.clone())
|
|
||||||
.ok_or((
|
|
||||||
StatusCode::NOT_FOUND,
|
|
||||||
other_error(format!("No such network instance: {}", inst_id)).into(),
|
|
||||||
))?;
|
|
||||||
|
|
||||||
Ok(serde_json::from_str::<NetworkConfig>(&db_row)
|
|
||||||
.map_err(|e| {
|
|
||||||
(
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
other_error(format!("Failed to parse network config: {:?}", e)).into(),
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build_route(&mut self) -> Router<AppStateInner> {
|
pub fn build_route(&mut self) -> Router<AppStateInner> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/api/v1/machines", get(Self::handle_list_machines))
|
.route("/api/v1/machines", get(Self::handle_list_machines))
|
||||||
|
@ -342,9 +311,5 @@ impl NetworkApi {
|
||||||
"/api/v1/machines/:machine-id/networks/info/:inst-id",
|
"/api/v1/machines/:machine-id/networks/info/:inst-id",
|
||||||
get(Self::handle_collect_one_network_info),
|
get(Self::handle_collect_one_network_info),
|
||||||
)
|
)
|
||||||
.route(
|
|
||||||
"/api/v1/machines/:machine-id/networks/config/:inst-id",
|
|
||||||
get(Self::handle_get_network_config),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,6 +120,9 @@ core_clap:
|
||||||
ipv6_listener:
|
ipv6_listener:
|
||||||
en: "the url of the ipv6 listener, e.g.: tcp://[::]:11010, if not set, will listen on random udp port"
|
en: "the url of the ipv6 listener, e.g.: tcp://[::]:11010, if not set, will listen on random udp port"
|
||||||
zh-CN: "IPv6 监听器的URL,例如:tcp://[::]:11010,如果未设置,将在随机UDP端口上监听"
|
zh-CN: "IPv6 监听器的URL,例如:tcp://[::]:11010,如果未设置,将在随机UDP端口上监听"
|
||||||
|
work_dir:
|
||||||
|
en: "Specify the working directory for the program. If not specified, the current directory will be used."
|
||||||
|
zh-CN: "指定程序的工作目录。如果未指定,将使用当前目录。"
|
||||||
|
|
||||||
core_app:
|
core_app:
|
||||||
panic_backtrace_save:
|
panic_backtrace_save:
|
||||||
|
|
|
@ -25,8 +25,6 @@ define_global_var!(OSPF_UPDATE_MY_GLOBAL_FOREIGN_NETWORK_INTERVAL_SEC, u64, 10);
|
||||||
|
|
||||||
pub const UDP_HOLE_PUNCH_CONNECTOR_SERVICE_ID: u32 = 2;
|
pub const UDP_HOLE_PUNCH_CONNECTOR_SERVICE_ID: u32 = 2;
|
||||||
|
|
||||||
pub const WIN_SERVICE_WORK_DIR_REG_KEY: &str = "SOFTWARE\\EasyTier\\Service\\WorkDir";
|
|
||||||
|
|
||||||
pub const EASYTIER_VERSION: &str = git_version::git_version!(
|
pub const EASYTIER_VERSION: &str = git_version::git_version!(
|
||||||
args = ["--abbrev=8", "--always", "--dirty=~"],
|
args = ["--abbrev=8", "--always", "--dirty=~"],
|
||||||
prefix = concat!(env!("CARGO_PKG_VERSION"), "-"),
|
prefix = concat!(env!("CARGO_PKG_VERSION"), "-"),
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
use std::{
|
use std::{
|
||||||
ffi::OsString, fmt::Write, net::SocketAddr, path::PathBuf, sync::Mutex, time::Duration, vec,
|
ffi::OsString,
|
||||||
|
net::SocketAddr,
|
||||||
|
path::PathBuf,
|
||||||
|
sync::Mutex,
|
||||||
|
time::Duration,
|
||||||
|
vec
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{Context, Ok};
|
use anyhow::{Context, Ok};
|
||||||
use clap::{command, Args, Parser, Subcommand};
|
use clap::{command, Args, Parser, Subcommand};
|
||||||
use humansize::format_size;
|
use humansize::format_size;
|
||||||
use service_manager::*;
|
|
||||||
use tabled::settings::Style;
|
use tabled::settings::Style;
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
|
use service_manager::*;
|
||||||
|
|
||||||
use easytier::{
|
use easytier::{
|
||||||
common::{
|
common::{
|
||||||
|
@ -57,7 +62,7 @@ enum SubCommand {
|
||||||
PeerCenter,
|
PeerCenter,
|
||||||
VpnPortal,
|
VpnPortal,
|
||||||
Node(NodeArgs),
|
Node(NodeArgs),
|
||||||
Service(ServiceArgs),
|
Service(ServiceArgs)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args, Debug)]
|
#[derive(Args, Debug)]
|
||||||
|
@ -126,11 +131,8 @@ struct NodeArgs {
|
||||||
|
|
||||||
#[derive(Args, Debug)]
|
#[derive(Args, Debug)]
|
||||||
struct ServiceArgs{
|
struct ServiceArgs{
|
||||||
#[arg(short, long, default_value = env!("CARGO_PKG_NAME"), help = "service name")]
|
|
||||||
name: String,
|
|
||||||
|
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
sub_command: ServiceSubCommand,
|
sub_command: ServiceSubCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand, Debug)]
|
#[derive(Subcommand, Debug)]
|
||||||
|
@ -139,28 +141,15 @@ enum ServiceSubCommand {
|
||||||
Uninstall,
|
Uninstall,
|
||||||
Status,
|
Status,
|
||||||
Start,
|
Start,
|
||||||
Stop,
|
Stop
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args, Debug)]
|
#[derive(Args, Debug)]
|
||||||
struct InstallArgs {
|
struct InstallArgs {
|
||||||
#[arg(long, default_value = env!("CARGO_PKG_DESCRIPTION"), help = "service description")]
|
|
||||||
description: String,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
display_name: Option<String>,
|
|
||||||
|
|
||||||
#[arg(long, default_value = "false")]
|
|
||||||
disable_autostart: bool,
|
|
||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
core_path: Option<PathBuf>,
|
core_path: Option<PathBuf>,
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
service_work_dir: Option<PathBuf>,
|
|
||||||
|
|
||||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||||
core_args: Option<Vec<OsString>>,
|
core_args: Option<Vec<OsString>>
|
||||||
}
|
}
|
||||||
|
|
||||||
type Error = anyhow::Error;
|
type Error = anyhow::Error;
|
||||||
|
@ -519,54 +508,48 @@ impl CommandHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ServiceInstallOptions {
|
|
||||||
pub program: PathBuf,
|
|
||||||
pub args: Vec<OsString>,
|
|
||||||
pub work_directory: PathBuf,
|
|
||||||
pub disable_autostart: bool,
|
|
||||||
pub description: Option<String>,
|
|
||||||
pub display_name: Option<String>,
|
|
||||||
}
|
|
||||||
pub struct Service{
|
pub struct Service{
|
||||||
lable: ServiceLabel,
|
lable: ServiceLabel,
|
||||||
kind: ServiceManagerKind,
|
service_manager: Box<dyn ServiceManager>
|
||||||
service_manager: Box<dyn ServiceManager>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Service {
|
impl Service {
|
||||||
pub fn new(name: String) -> Result<Self, Error> {
|
pub fn new() -> Result<Self, Error> {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
let service_manager = Box::new(crate::win_service_manager::WinServiceManager::new()?);
|
let service_manager = Box::new(
|
||||||
|
crate::win_service_manager::WinServiceManager::new(
|
||||||
|
Some(OsString::from("EasyTier Service")),
|
||||||
|
Some(OsString::from(env!("CARGO_PKG_DESCRIPTION"))),
|
||||||
|
vec![OsString::from("dnscache"), OsString::from("rpcss")],
|
||||||
|
)?
|
||||||
|
);
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
let service_manager = <dyn ServiceManager>::native()?;
|
let service_manager = <dyn ServiceManager>::native()?;
|
||||||
let kind = ServiceManagerKind::native()?;
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
lable: name.parse()?,
|
lable: env!("CARGO_PKG_NAME").parse().unwrap(),
|
||||||
kind,
|
service_manager
|
||||||
service_manager,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn install(&self, options: &ServiceInstallOptions) -> Result<(), Error> {
|
pub fn install(&self, bin_path: std::path::PathBuf, bin_args: Vec<OsString>) -> Result<(), Error> {
|
||||||
let ctx = ServiceInstallCtx {
|
let ctx = ServiceInstallCtx {
|
||||||
label: self.lable.clone(),
|
label: self.lable.clone(),
|
||||||
program: options.program.clone(),
|
contents: None,
|
||||||
args: options.args.clone(),
|
program: bin_path,
|
||||||
contents: self.make_install_content_option(options),
|
args: bin_args,
|
||||||
autostart: !options.disable_autostart,
|
autostart: true,
|
||||||
username: None,
|
username: None,
|
||||||
working_directory: Some(options.work_directory.clone()),
|
working_directory: None,
|
||||||
environment: None,
|
environment: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
if self.status()? != ServiceStatus::NotInstalled {
|
if self.status()? != ServiceStatus::NotInstalled {
|
||||||
return Err(anyhow::anyhow!("Service is already installed"));
|
return Err(anyhow::anyhow!("Service is already installed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.service_manager
|
self.service_manager.install(ctx).map_err(|e| anyhow::anyhow!("failed to install service: {}", e))
|
||||||
.install(ctx)
|
|
||||||
.map_err(|e| anyhow::anyhow!("failed to install service: {}", e))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn uninstall(&self) -> Result<(), Error> {
|
pub fn uninstall(&self) -> Result<(), Error> {
|
||||||
|
@ -585,9 +568,7 @@ impl Service {
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.service_manager
|
self.service_manager.uninstall(ctx).map_err(|e| anyhow::anyhow!("failed to uninstall service: {}", e))
|
||||||
.uninstall(ctx)
|
|
||||||
.map_err(|e| anyhow::anyhow!("failed to uninstall service: {}", e))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn status(&self) -> Result<ServiceStatus, Error> {
|
pub fn status(&self) -> Result<ServiceStatus, Error> {
|
||||||
|
@ -606,14 +587,16 @@ impl Service {
|
||||||
let status = self.status()?;
|
let status = self.status()?;
|
||||||
|
|
||||||
match status {
|
match status {
|
||||||
ServiceStatus::Running => Err(anyhow::anyhow!("Service is already running")),
|
ServiceStatus::Running => {
|
||||||
|
Err(anyhow::anyhow!("Service is already running"))
|
||||||
|
}
|
||||||
ServiceStatus::Stopped(_) => {
|
ServiceStatus::Stopped(_) => {
|
||||||
self.service_manager
|
self.service_manager.start(ctx).map_err(|e| anyhow::anyhow!("failed to start service: {}", e))?;
|
||||||
.start(ctx)
|
|
||||||
.map_err(|e| anyhow::anyhow!("failed to start service: {}", e))?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
ServiceStatus::NotInstalled => Err(anyhow::anyhow!("Service is not installed")),
|
ServiceStatus::NotInstalled => {
|
||||||
|
Err(anyhow::anyhow!("Service is not installed"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -625,149 +608,18 @@ impl Service {
|
||||||
|
|
||||||
match status {
|
match status {
|
||||||
ServiceStatus::Running => {
|
ServiceStatus::Running => {
|
||||||
self.service_manager
|
self.service_manager.stop(ctx).map_err(|e| anyhow::anyhow!("failed to stop service: {}", e))?;
|
||||||
.stop(ctx)
|
|
||||||
.map_err(|e| anyhow::anyhow!("failed to stop service: {}", e))?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
ServiceStatus::Stopped(_) => Err(anyhow::anyhow!("Service is already stopped")),
|
ServiceStatus::Stopped(_) => {
|
||||||
ServiceStatus::NotInstalled => Err(anyhow::anyhow!("Service is not installed")),
|
Err(anyhow::anyhow!("Service is already stopped"))
|
||||||
}
|
}
|
||||||
}
|
ServiceStatus::NotInstalled => {
|
||||||
|
Err(anyhow::anyhow!("Service is not installed"))
|
||||||
fn make_install_content_option(&self, options: &ServiceInstallOptions) -> Option<String> {
|
|
||||||
match self.kind {
|
|
||||||
ServiceManagerKind::Systemd => Some(self.make_systemd_unit(options).unwrap()),
|
|
||||||
ServiceManagerKind::Rcd => Some(self.make_rcd_script(options).unwrap()),
|
|
||||||
ServiceManagerKind::OpenRc => Some(self.make_open_rc_script(options).unwrap()),
|
|
||||||
_ => {
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
let win_options = win_service_manager::WinServiceInstallOptions {
|
|
||||||
description: options.description.clone(),
|
|
||||||
display_name: options.display_name.clone(),
|
|
||||||
dependencies: Some(vec!["rpcss".to_string(), "dnscache".to_string()]),
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(serde_json::to_string(&win_options).unwrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_systemd_unit(
|
|
||||||
&self,
|
|
||||||
options: &ServiceInstallOptions,
|
|
||||||
) -> Result<String, std::fmt::Error> {
|
|
||||||
let args = options
|
|
||||||
.args
|
|
||||||
.iter()
|
|
||||||
.map(|a| a.to_string_lossy())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(" ");
|
|
||||||
let target_app = options.program.display().to_string();
|
|
||||||
let work_dir = options.work_directory.display().to_string();
|
|
||||||
let mut unit_content = String::new();
|
|
||||||
|
|
||||||
writeln!(unit_content, "[Unit]")?;
|
|
||||||
writeln!(unit_content, "After=network.target syslog.target")?;
|
|
||||||
if let Some(ref d) = options.description {
|
|
||||||
writeln!(unit_content, "Description={d}")?;
|
|
||||||
}
|
|
||||||
writeln!(unit_content, "StartLimitIntervalSec=0")?;
|
|
||||||
writeln!(unit_content)?;
|
|
||||||
writeln!(unit_content, "[Service]")?;
|
|
||||||
writeln!(unit_content, "Type=simple")?;
|
|
||||||
writeln!(unit_content, "WorkingDirectory={work_dir}")?;
|
|
||||||
writeln!(unit_content, "ExecStart={target_app} {args}")?;
|
|
||||||
writeln!(unit_content, "Restart=Always")?;
|
|
||||||
writeln!(unit_content, "LimitNOFILE=infinity")?;
|
|
||||||
writeln!(unit_content)?;
|
|
||||||
writeln!(unit_content, "[Install]")?;
|
|
||||||
writeln!(unit_content, "WantedBy=multi-user.target")?;
|
|
||||||
|
|
||||||
std::result::Result::Ok(unit_content)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_rcd_script(&self, options: &ServiceInstallOptions) -> Result<String, std::fmt::Error> {
|
|
||||||
let name = self.lable.to_qualified_name();
|
|
||||||
let args = options
|
|
||||||
.args
|
|
||||||
.iter()
|
|
||||||
.map(|a| a.to_string_lossy())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(" ");
|
|
||||||
let target_app = options.program.display().to_string();
|
|
||||||
let work_dir = options.work_directory.display().to_string();
|
|
||||||
let mut script = String::new();
|
|
||||||
|
|
||||||
writeln!(script, "#!/bin/sh")?;
|
|
||||||
writeln!(script, "#")?;
|
|
||||||
writeln!(script, "# PROVIDE: {name}")?;
|
|
||||||
writeln!(script, "# REQUIRE: LOGIN FILESYSTEMS NETWORKING ")?;
|
|
||||||
writeln!(script, "# KEYWORD: shutdown")?;
|
|
||||||
writeln!(script)?;
|
|
||||||
writeln!(script, ". /etc/rc.subr")?;
|
|
||||||
writeln!(script)?;
|
|
||||||
writeln!(script, "name=\"{name}\"")?;
|
|
||||||
if let Some(ref d) = options.description {
|
|
||||||
writeln!(script, "desc=\"{d}\"")?;
|
|
||||||
}
|
|
||||||
writeln!(script, "rcvar=\"{name}_enable\"")?;
|
|
||||||
writeln!(script)?;
|
|
||||||
writeln!(script, "load_rc_config ${{name}}")?;
|
|
||||||
writeln!(script)?;
|
|
||||||
writeln!(script, ": ${{{name}_options=\"{args}\"}}")?;
|
|
||||||
writeln!(script)?;
|
|
||||||
writeln!(script, "{name}_chdir=\"{work_dir}\"")?;
|
|
||||||
writeln!(script, "pidfile=\"/var/run/${{name}}.pid\"")?;
|
|
||||||
writeln!(script, "procname=\"{target_app}\"")?;
|
|
||||||
writeln!(script, "command=\"/usr/sbin/daemon\"")?;
|
|
||||||
writeln!(
|
|
||||||
script,
|
|
||||||
"command_args=\"-c -S -T ${{name}} -p ${{pidfile}} ${{procname}} ${{{name}_options}}\""
|
|
||||||
)?;
|
|
||||||
writeln!(script)?;
|
|
||||||
writeln!(script, "run_rc_command \"$1\"")?;
|
|
||||||
|
|
||||||
std::result::Result::Ok(script)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_open_rc_script(
|
|
||||||
&self,
|
|
||||||
options: &ServiceInstallOptions,
|
|
||||||
) -> Result<String, std::fmt::Error> {
|
|
||||||
let args = options
|
|
||||||
.args
|
|
||||||
.iter()
|
|
||||||
.map(|a| a.to_string_lossy())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(" ");
|
|
||||||
let target_app = options.program.display().to_string();
|
|
||||||
let work_dir = options.work_directory.display().to_string();
|
|
||||||
let mut script = String::new();
|
|
||||||
|
|
||||||
writeln!(script, "#!/sbin/openrc-run")?;
|
|
||||||
writeln!(script)?;
|
|
||||||
if let Some(ref d) = options.description {
|
|
||||||
writeln!(script, "description=\"{d}\"")?;
|
|
||||||
}
|
|
||||||
writeln!(script, "command=\"{target_app}\"")?;
|
|
||||||
writeln!(script, "command_args=\"{args}\"")?;
|
|
||||||
writeln!(script, "pidfile=\"/run/${{RC_SVCNAME}}.pid\"")?;
|
|
||||||
writeln!(script, "command_background=\"yes\"")?;
|
|
||||||
writeln!(script, "directory=\"{work_dir}\"")?;
|
|
||||||
writeln!(script)?;
|
|
||||||
writeln!(script, "depend() {{")?;
|
|
||||||
writeln!(script, " need net")?;
|
|
||||||
writeln!(script, " use looger")?;
|
|
||||||
writeln!(script, "}}")?;
|
|
||||||
|
|
||||||
std::result::Result::Ok(script)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
@ -933,7 +785,7 @@ async fn main() -> Result<(), Error> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SubCommand::Service(service_args) => {
|
SubCommand::Service(service_args) => {
|
||||||
let service = Service::new(service_args.name)?;
|
let service = Service::new()?;
|
||||||
match service_args.sub_command {
|
match service_args.sub_command {
|
||||||
ServiceSubCommand::Install(install_args) => {
|
ServiceSubCommand::Install(install_args) => {
|
||||||
let bin_path = install_args.core_path.unwrap_or_else(|| {
|
let bin_path = install_args.core_path.unwrap_or_else(|| {
|
||||||
|
@ -953,35 +805,7 @@ async fn main() -> Result<(), Error> {
|
||||||
anyhow::anyhow!("failed to get easytier core application: {}", e)
|
anyhow::anyhow!("failed to get easytier core application: {}", e)
|
||||||
})?;
|
})?;
|
||||||
let bin_args = install_args.core_args.unwrap_or_default();
|
let bin_args = install_args.core_args.unwrap_or_default();
|
||||||
let work_dir = install_args.service_work_dir.unwrap_or_else(|| {
|
service.install(bin_path, bin_args)?;
|
||||||
if cfg!(target_os = "windows") {
|
|
||||||
bin_path.parent().unwrap().to_path_buf()
|
|
||||||
} else {
|
|
||||||
std::env::temp_dir()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let work_dir = std::fs::canonicalize(&work_dir).map_err(|e| {
|
|
||||||
anyhow::anyhow!(
|
|
||||||
"failed to get service work directory[{}]: {}",
|
|
||||||
work_dir.display(),
|
|
||||||
e
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if !work_dir.is_dir() {
|
|
||||||
return Err(anyhow::anyhow!("work directory is not a directory"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let install_options = ServiceInstallOptions {
|
|
||||||
program: bin_path,
|
|
||||||
args: bin_args,
|
|
||||||
work_directory: work_dir,
|
|
||||||
disable_autostart: install_args.disable_autostart,
|
|
||||||
description: Some(install_args.description),
|
|
||||||
display_name: install_args.display_name,
|
|
||||||
};
|
|
||||||
service.install(&install_options)?;
|
|
||||||
}
|
}
|
||||||
ServiceSubCommand::Uninstall => {
|
ServiceSubCommand::Uninstall => {
|
||||||
service.uninstall()?;
|
service.uninstall()?;
|
||||||
|
@ -1009,42 +833,55 @@ async fn main() -> Result<(), Error> {
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
mod win_service_manager {
|
mod win_service_manager {
|
||||||
use std::{ffi::OsStr, ffi::OsString, io, path::PathBuf};
|
|
||||||
use windows_service::{
|
use windows_service::{
|
||||||
service::{
|
service::{
|
||||||
ServiceAccess, ServiceDependency, ServiceErrorControl, ServiceInfo, ServiceStartType,
|
|
||||||
ServiceType,
|
ServiceType,
|
||||||
|
ServiceErrorControl,
|
||||||
|
ServiceDependency,
|
||||||
|
ServiceInfo,
|
||||||
|
ServiceStartType,
|
||||||
|
ServiceAccess
|
||||||
},
|
},
|
||||||
service_manager::{ServiceManager, ServiceManagerAccess},
|
service_manager::{
|
||||||
|
ServiceManagerAccess,
|
||||||
|
ServiceManager
|
||||||
|
}
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
io,
|
||||||
|
ffi::OsString,
|
||||||
|
ffi::OsStr
|
||||||
};
|
};
|
||||||
|
|
||||||
use service_manager::{
|
use service_manager::{
|
||||||
ServiceInstallCtx, ServiceLevel, ServiceStartCtx, ServiceStatus, ServiceStatusCtx,
|
ServiceInstallCtx,
|
||||||
ServiceStopCtx, ServiceUninstallCtx,
|
ServiceLevel,
|
||||||
|
ServiceStartCtx,
|
||||||
|
ServiceStatus,
|
||||||
|
ServiceStatusCtx,
|
||||||
|
ServiceUninstallCtx,
|
||||||
|
ServiceStopCtx
|
||||||
};
|
};
|
||||||
|
|
||||||
use winreg::{enums::*, RegKey};
|
|
||||||
|
|
||||||
use easytier::common::constants::WIN_SERVICE_WORK_DIR_REG_KEY;
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub struct WinServiceInstallOptions {
|
|
||||||
pub dependencies: Option<Vec<String>>,
|
|
||||||
pub description: Option<String>,
|
|
||||||
pub display_name: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct WinServiceManager {
|
pub struct WinServiceManager {
|
||||||
service_manager: ServiceManager,
|
service_manager: ServiceManager,
|
||||||
|
display_name: Option<OsString>,
|
||||||
|
description: Option<OsString>,
|
||||||
|
dependencies: Vec<OsString>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WinServiceManager {
|
impl WinServiceManager {
|
||||||
pub fn new() -> Result<Self, crate::Error> {
|
pub fn new(display_name: Option<OsString>, description: Option<OsString>, dependencies: Vec<OsString>,) -> Result<Self, crate::Error> {
|
||||||
let service_manager =
|
let service_manager = ServiceManager::local_computer(
|
||||||
ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::ALL_ACCESS)?;
|
None::<&str>,
|
||||||
Ok(Self { service_manager })
|
ServiceManagerAccess::ALL_ACCESS,
|
||||||
|
)?;
|
||||||
|
Ok(Self {
|
||||||
|
service_manager,
|
||||||
|
display_name,
|
||||||
|
description,
|
||||||
|
dependencies,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl service_manager::ServiceManager for WinServiceManager {
|
impl service_manager::ServiceManager for WinServiceManager {
|
||||||
|
@ -1053,32 +890,10 @@ mod win_service_manager {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn install(&self, ctx: ServiceInstallCtx) -> io::Result<()> {
|
fn install(&self, ctx: ServiceInstallCtx) -> io::Result<()> {
|
||||||
let start_type_ = if ctx.autostart {
|
let start_type_ = if ctx.autostart { ServiceStartType::AutoStart } else { ServiceStartType::OnDemand };
|
||||||
ServiceStartType::AutoStart
|
|
||||||
} else {
|
|
||||||
ServiceStartType::OnDemand
|
|
||||||
};
|
|
||||||
let srv_name = OsString::from(ctx.label.to_qualified_name());
|
let srv_name = OsString::from(ctx.label.to_qualified_name());
|
||||||
let mut dis_name = srv_name.clone();
|
let dis_name = self.display_name.clone().unwrap_or_else(|| srv_name.clone());
|
||||||
let mut description: Option<OsString> = None;
|
let dependencies = self.dependencies.iter().map(|dep| ServiceDependency::Service(dep.clone())).collect::<Vec<_>>();
|
||||||
let mut dependencies = Vec::<ServiceDependency>::new();
|
|
||||||
|
|
||||||
if let Some(s) = ctx.contents.as_ref() {
|
|
||||||
let options: WinServiceInstallOptions = serde_json::from_str(s.as_str()).unwrap();
|
|
||||||
if let Some(d) = options.dependencies {
|
|
||||||
dependencies = d
|
|
||||||
.iter()
|
|
||||||
.map(|dep| ServiceDependency::Service(OsString::from(dep.clone())))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
}
|
|
||||||
if let Some(d) = options.description {
|
|
||||||
description = Some(OsString::from(d));
|
|
||||||
}
|
|
||||||
if let Some(d) = options.display_name {
|
|
||||||
dis_name = OsString::from(d);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let service_info = ServiceInfo {
|
let service_info = ServiceInfo {
|
||||||
name: srv_name,
|
name: srv_name,
|
||||||
display_name: dis_name,
|
display_name: dis_name,
|
||||||
|
@ -1089,58 +904,50 @@ mod win_service_manager {
|
||||||
launch_arguments: ctx.args,
|
launch_arguments: ctx.args,
|
||||||
dependencies: dependencies.clone(),
|
dependencies: dependencies.clone(),
|
||||||
account_name: None,
|
account_name: None,
|
||||||
account_password: None,
|
account_password: None
|
||||||
};
|
};
|
||||||
|
|
||||||
let service = self
|
let service = self.service_manager.create_service(&service_info, ServiceAccess::ALL_ACCESS).map_err(|e| {
|
||||||
.service_manager
|
io::Error::new(io::ErrorKind::Other, e)
|
||||||
.create_service(&service_info, ServiceAccess::ALL_ACCESS)
|
})?;
|
||||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
|
||||||
|
|
||||||
if let Some(s) = description {
|
if let Some(s) = &self.description {
|
||||||
service
|
service.set_description(s.clone()).map_err(|e| {
|
||||||
.set_description(s.clone())
|
io::Error::new(io::ErrorKind::Other, e)
|
||||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
})?;
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(work_dir) = ctx.working_directory {
|
|
||||||
set_service_work_directory(&ctx.label.to_qualified_name(), work_dir)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn uninstall(&self, ctx: ServiceUninstallCtx) -> io::Result<()> {
|
fn uninstall(&self, ctx: ServiceUninstallCtx) -> io::Result<()> {
|
||||||
let service = self
|
let service = self.service_manager.open_service(ctx.label.to_qualified_name(), ServiceAccess::ALL_ACCESS).map_err(|e|{
|
||||||
.service_manager
|
io::Error::new(io::ErrorKind::Other, e)
|
||||||
.open_service(ctx.label.to_qualified_name(), ServiceAccess::ALL_ACCESS)
|
})?;
|
||||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
|
||||||
|
|
||||||
service
|
service.delete().map_err(|e|{
|
||||||
.delete()
|
io::Error::new(io::ErrorKind::Other, e)
|
||||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start(&self, ctx: ServiceStartCtx) -> io::Result<()> {
|
fn start(&self, ctx: ServiceStartCtx) -> io::Result<()> {
|
||||||
let service = self
|
let service = self.service_manager.open_service(ctx.label.to_qualified_name(), ServiceAccess::ALL_ACCESS).map_err(|e|{
|
||||||
.service_manager
|
io::Error::new(io::ErrorKind::Other, e)
|
||||||
.open_service(ctx.label.to_qualified_name(), ServiceAccess::ALL_ACCESS)
|
})?;
|
||||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
|
||||||
|
|
||||||
service
|
service.start(&[] as &[&OsStr]).map_err(|e|{
|
||||||
.start(&[] as &[&OsStr])
|
io::Error::new(io::ErrorKind::Other, e)
|
||||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stop(&self, ctx: ServiceStopCtx) -> io::Result<()> {
|
fn stop(&self, ctx: ServiceStopCtx) -> io::Result<()> {
|
||||||
let service = self
|
let service = self.service_manager.open_service(ctx.label.to_qualified_name(), ServiceAccess::ALL_ACCESS).map_err(|e|{
|
||||||
.service_manager
|
io::Error::new(io::ErrorKind::Other, e)
|
||||||
.open_service(ctx.label.to_qualified_name(), ServiceAccess::ALL_ACCESS)
|
})?;
|
||||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
|
||||||
|
|
||||||
_ = service
|
_ = service.stop().map_err(|e|{
|
||||||
.stop()
|
io::Error::new(io::ErrorKind::Other, e)
|
||||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1152,18 +959,12 @@ mod win_service_manager {
|
||||||
fn set_level(&mut self, level: ServiceLevel) -> io::Result<()> {
|
fn set_level(&mut self, level: ServiceLevel) -> io::Result<()> {
|
||||||
match level {
|
match level {
|
||||||
ServiceLevel::System => Ok(()),
|
ServiceLevel::System => Ok(()),
|
||||||
_ => Err(io::Error::new(
|
_ => Err(io::Error::new(io::ErrorKind::Other, "Unsupported service level"))
|
||||||
io::ErrorKind::Other,
|
|
||||||
"Unsupported service level",
|
|
||||||
)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn status(&self, ctx: ServiceStatusCtx) -> io::Result<ServiceStatus> {
|
fn status(&self, ctx: ServiceStatusCtx) -> io::Result<ServiceStatus> {
|
||||||
let service = match self
|
let service = match self.service_manager.open_service(ctx.label.to_qualified_name(), ServiceAccess::QUERY_STATUS) {
|
||||||
.service_manager
|
|
||||||
.open_service(ctx.label.to_qualified_name(), ServiceAccess::QUERY_STATUS)
|
|
||||||
{
|
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if let windows_service::Error::Winapi(ref win_err) = e {
|
if let windows_service::Error::Winapi(ref win_err) = e {
|
||||||
|
@ -1175,9 +976,9 @@ mod win_service_manager {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let status = service
|
let status = service.query_status().map_err(|e|{
|
||||||
.query_status()
|
io::Error::new(io::ErrorKind::Other, e)
|
||||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
})?;
|
||||||
|
|
||||||
match status.current_state {
|
match status.current_state {
|
||||||
windows_service::service::ServiceState::Stopped => Ok(ServiceStatus::Stopped(None)),
|
windows_service::service::ServiceState::Stopped => Ok(ServiceStatus::Stopped(None)),
|
||||||
|
@ -1185,12 +986,4 @@ mod win_service_manager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_service_work_directory(service_name: &str, work_directory: PathBuf) -> io::Result<()> {
|
|
||||||
let (reg_key, _) =
|
|
||||||
RegKey::predef(HKEY_LOCAL_MACHINE).create_subkey(WIN_SERVICE_WORK_DIR_REG_KEY)?;
|
|
||||||
reg_key
|
|
||||||
.set_value::<OsString, _>(service_name, &work_directory.as_os_str().to_os_string())?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -289,6 +289,12 @@ struct Cli {
|
||||||
help = t!("core_clap.ipv6_listener").to_string()
|
help = t!("core_clap.ipv6_listener").to_string()
|
||||||
)]
|
)]
|
||||||
ipv6_listener: Option<String>,
|
ipv6_listener: Option<String>,
|
||||||
|
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = t!("core_clap.work_dir").to_string()
|
||||||
|
)]
|
||||||
|
work_dir: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
rust_i18n::i18n!("locales", fallback = "en");
|
rust_i18n::i18n!("locales", fallback = "en");
|
||||||
|
@ -357,7 +363,7 @@ impl Cli {
|
||||||
}
|
}
|
||||||
return Ok("0.0.0.0:0".parse().unwrap());
|
return Ok("0.0.0.0:0".parse().unwrap());
|
||||||
}
|
}
|
||||||
return Ok(format!("0.0.0.0:{}", port).parse().unwrap());
|
return Ok("0.0.0.0:0".parse().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(rpc_portal.parse()?)
|
Ok(rpc_portal.parse()?)
|
||||||
|
@ -374,32 +380,28 @@ impl TryFrom<&Cli> for TomlConfigLoader {
|
||||||
config_file
|
config_file
|
||||||
);
|
);
|
||||||
return Ok(TomlConfigLoader::new(config_file)
|
return Ok(TomlConfigLoader::new(config_file)
|
||||||
.with_context(|| format!("failed to load config file: {:?}", cli.config_file))?);
|
.with_context(|| format!("failed to load config file: {:?}", cli.config_file))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
let cfg = TomlConfigLoader::default();
|
let cfg = TomlConfigLoader::default();
|
||||||
|
|
||||||
cfg.set_hostname(cli.hostname.clone());
|
cfg.set_hostname(cli.hostname.clone());
|
||||||
|
|
||||||
cfg.set_network_identity(NetworkIdentity::new(
|
cfg.set_network_identity(NetworkIdentity::new(cli.network_name.clone(), cli.network_secret.clone()));
|
||||||
cli.network_name.clone(),
|
|
||||||
cli.network_secret.clone(),
|
|
||||||
));
|
|
||||||
|
|
||||||
cfg.set_dhcp(cli.dhcp);
|
cfg.set_dhcp(cli.dhcp);
|
||||||
|
|
||||||
if let Some(ipv4) = &cli.ipv4 {
|
if let Some(ipv4) = &cli.ipv4 {
|
||||||
cfg.set_ipv4(Some(ipv4.parse().with_context(|| {
|
cfg.set_ipv4(Some(
|
||||||
format!("failed to parse ipv4 address: {}", ipv4)
|
ipv4.parse()
|
||||||
})?))
|
.with_context(|| format!("failed to parse ipv4 address: {}", ipv4))?
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut peers = Vec::<PeerConfig>::with_capacity(cli.peers.len());
|
let mut peers = Vec::<PeerConfig>::with_capacity(cli.peers.len());
|
||||||
for p in &cli.peers {
|
for p in &cli.peers {
|
||||||
peers.push(PeerConfig {
|
peers.push(PeerConfig {
|
||||||
uri: p
|
uri: p.parse().with_context(|| format!("failed to parse peer uri: {}", p))?,
|
||||||
.parse()
|
|
||||||
.with_context(|| format!("failed to parse peer uri: {}", p))?,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
cfg.set_peers(peers);
|
cfg.set_peers(peers);
|
||||||
|
@ -414,21 +416,22 @@ impl TryFrom<&Cli> for TomlConfigLoader {
|
||||||
for n in cli.proxy_networks.iter() {
|
for n in cli.proxy_networks.iter() {
|
||||||
cfg.add_proxy_cidr(
|
cfg.add_proxy_cidr(
|
||||||
n.parse()
|
n.parse()
|
||||||
.with_context(|| format!("failed to parse proxy network: {}", n))?,
|
.with_context(|| format!("failed to parse proxy network: {}", n))?
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.set_rpc_portal(
|
cfg.set_rpc_portal(Cli::parse_rpc_portal(cli.rpc_portal.clone()).with_context(|| {
|
||||||
Cli::parse_rpc_portal(cli.rpc_portal.clone())
|
format!("failed to parse rpc portal: {}", cli.rpc_portal)
|
||||||
.with_context(|| format!("failed to parse rpc portal: {}", cli.rpc_portal))?,
|
})?);
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(external_nodes) = cli.external_node.as_ref() {
|
if let Some(external_nodes) = cli.external_node.as_ref() {
|
||||||
let mut old_peers = cfg.get_peers();
|
let mut old_peers = cfg.get_peers();
|
||||||
old_peers.push(PeerConfig {
|
old_peers.push(PeerConfig {
|
||||||
uri: external_nodes.parse().with_context(|| {
|
uri: external_nodes
|
||||||
|
.parse()
|
||||||
|
.with_context(|| {
|
||||||
format!("failed to parse external node uri: {}", external_nodes)
|
format!("failed to parse external node uri: {}", external_nodes)
|
||||||
})?,
|
})?
|
||||||
});
|
});
|
||||||
cfg.set_peers(old_peers);
|
cfg.set_peers(old_peers);
|
||||||
}
|
}
|
||||||
|
@ -453,15 +456,11 @@ impl TryFrom<&Cli> for TomlConfigLoader {
|
||||||
let url: url::Url = vpn_portal
|
let url: url::Url = vpn_portal
|
||||||
.parse()
|
.parse()
|
||||||
.with_context(|| format!("failed to parse vpn portal url: {}", vpn_portal))?;
|
.with_context(|| format!("failed to parse vpn portal url: {}", vpn_portal))?;
|
||||||
let host = url
|
let host = url.host_str().ok_or_else(|| anyhow::anyhow!("vpn portal url missing host"))?;
|
||||||
.host_str()
|
let port = url.port().ok_or_else(|| anyhow::anyhow!("vpn portal url missing port"))?;
|
||||||
.ok_or_else(|| anyhow::anyhow!("vpn portal url missing host"))?;
|
let client_cidr = url.path()[1..]
|
||||||
let port = url
|
.parse()
|
||||||
.port()
|
.with_context(|| format!("failed to parse vpn portal client cidr: {}", url.path()))?;
|
||||||
.ok_or_else(|| anyhow::anyhow!("vpn portal url missing port"))?;
|
|
||||||
let client_cidr = url.path()[1..].parse().with_context(|| {
|
|
||||||
format!("failed to parse vpn portal client cidr: {}", url.path())
|
|
||||||
})?;
|
|
||||||
let wireguard_listen: SocketAddr = format!("{}:{}", host, port).parse().unwrap();
|
let wireguard_listen: SocketAddr = format!("{}:{}", host, port).parse().unwrap();
|
||||||
cfg.set_vpn_portal_config(VpnPortalConfig {
|
cfg.set_vpn_portal_config(VpnPortalConfig {
|
||||||
wireguard_listen,
|
wireguard_listen,
|
||||||
|
@ -472,10 +471,7 @@ impl TryFrom<&Cli> for TomlConfigLoader {
|
||||||
if let Some(manual_routes) = cli.manual_routes.as_ref() {
|
if let Some(manual_routes) = cli.manual_routes.as_ref() {
|
||||||
let mut routes = Vec::<cidr::Ipv4Cidr>::with_capacity(manual_routes.len());
|
let mut routes = Vec::<cidr::Ipv4Cidr>::with_capacity(manual_routes.len());
|
||||||
for r in manual_routes{
|
for r in manual_routes{
|
||||||
routes.push(
|
routes.push(r.parse().with_context(|| format!("failed to parse route: {}", r))?);
|
||||||
r.parse()
|
|
||||||
.with_context(|| format!("failed to parse route: {}", r))?,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
cfg.set_routes(Some(routes));
|
cfg.set_routes(Some(routes));
|
||||||
}
|
}
|
||||||
|
@ -644,30 +640,14 @@ pub fn handle_event(mut events: EventBusSubscriber) -> tokio::task::JoinHandle<(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
fn win_service_set_work_dir(service_name: &std::ffi::OsString) -> anyhow::Result<()> {
|
|
||||||
use easytier::common::constants::WIN_SERVICE_WORK_DIR_REG_KEY;
|
|
||||||
use winreg::enums::*;
|
|
||||||
use winreg::RegKey;
|
|
||||||
|
|
||||||
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
|
||||||
let key = hklm.open_subkey_with_flags(WIN_SERVICE_WORK_DIR_REG_KEY, KEY_READ)?;
|
|
||||||
let dir_pat_str = key.get_value::<std::ffi::OsString, _>(service_name)?;
|
|
||||||
let dir_path = std::fs::canonicalize(dir_pat_str)?;
|
|
||||||
|
|
||||||
std::env::set_current_dir(dir_path)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
fn win_service_event_loop(
|
fn win_service_event_loop(
|
||||||
stop_notify: std::sync::Arc<tokio::sync::Notify>,
|
stop_notify: std::sync::Arc<tokio::sync::Notify>,
|
||||||
cli: Cli,
|
cli: Cli,
|
||||||
status_handle: windows_service::service_control_handler::ServiceStatusHandle,
|
status_handle: windows_service::service_control_handler::ServiceStatusHandle,
|
||||||
) {
|
) {
|
||||||
use std::time::Duration;
|
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
|
use std::time::Duration;
|
||||||
use windows_service::service::*;
|
use windows_service::service::*;
|
||||||
|
|
||||||
let normal_status = ServiceStatus {
|
let normal_status = ServiceStatus {
|
||||||
|
@ -689,6 +669,12 @@ fn win_service_event_loop(
|
||||||
process_id: None,
|
process_id: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if cli.work_dir == None {
|
||||||
|
let mut path = std::env::current_exe().unwrap();
|
||||||
|
path.pop();
|
||||||
|
std::env::set_current_dir(path).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let rt = Runtime::new().unwrap();
|
let rt = Runtime::new().unwrap();
|
||||||
rt.block_on(async move {
|
rt.block_on(async move {
|
||||||
|
@ -715,15 +701,13 @@ fn win_service_event_loop(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
fn win_service_main(arg: Vec<std::ffi::OsString>) {
|
fn win_service_main(_: Vec<std::ffi::OsString>) {
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::sync::Notify;
|
use tokio::sync::Notify;
|
||||||
use windows_service::service::*;
|
use windows_service::service::*;
|
||||||
use windows_service::service_control_handler::*;
|
use windows_service::service_control_handler::*;
|
||||||
|
|
||||||
_ = win_service_set_work_dir(&arg[0]);
|
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
let stop_notify_send = Arc::new(Notify::new());
|
let stop_notify_send = Arc::new(Notify::new());
|
||||||
|
@ -748,16 +732,15 @@ fn win_service_main(arg: Vec<std::ffi::OsString>) {
|
||||||
wait_hint: Duration::default(),
|
wait_hint: Duration::default(),
|
||||||
process_id: None,
|
process_id: None,
|
||||||
};
|
};
|
||||||
status_handle
|
status_handle.set_service_status(next_status).expect("set service status fail");
|
||||||
.set_service_status(next_status)
|
|
||||||
.expect("set service status fail");
|
|
||||||
|
|
||||||
win_service_event_loop(stop_notify_recv, cli, status_handle);
|
win_service_event_loop(stop_notify_recv, cli, status_handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_main(cli: Cli) -> anyhow::Result<()> {
|
async fn run_main(cli: Cli) -> anyhow::Result<()> {
|
||||||
let cfg = TomlConfigLoader::try_from(&cli)?;
|
if let Some(dir) = cli.work_dir.as_ref() {
|
||||||
init_logger(&cfg, false)?;
|
std::env::set_current_dir(dir).map_err(|e| anyhow::anyhow!("failed to set work dir: {}", e))?;
|
||||||
|
}
|
||||||
|
|
||||||
if cli.config_server.is_some() {
|
if cli.config_server.is_some() {
|
||||||
let config_server_url_s = cli.config_server.clone().unwrap();
|
let config_server_url_s = cli.config_server.clone().unwrap();
|
||||||
|
@ -793,6 +776,9 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let cfg = TomlConfigLoader::try_from(&cli)?;
|
||||||
|
init_logger(&cfg, false)?;
|
||||||
|
|
||||||
println!("Starting easytier with config:");
|
println!("Starting easytier with config:");
|
||||||
println!("############### TOML ###############\n");
|
println!("############### TOML ###############\n");
|
||||||
println!("{}", cfg.dump());
|
println!("{}", cfg.dump());
|
||||||
|
@ -801,7 +787,7 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> {
|
||||||
let mut l = launcher::NetworkInstance::new(cfg).set_fetch_node_info(false);
|
let mut l = launcher::NetworkInstance::new(cfg).set_fetch_node_info(false);
|
||||||
let _t = ScopedTask::from(handle_event(l.start().unwrap()));
|
let _t = ScopedTask::from(handle_event(l.start().unwrap()));
|
||||||
if let Some(e) = l.wait().await{
|
if let Some(e) = l.wait().await{
|
||||||
anyhow::bail!("launcher error: {}", e);
|
return Err(anyhow::anyhow!("launcher error: {}", e));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -815,12 +801,11 @@ async fn main() {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
match windows_service::service_dispatcher::start(String::new(), ffi_service_main) {
|
match windows_service::service_dispatcher::start(String::new(), ffi_service_main) {
|
||||||
Ok(_) => std::thread::park(),
|
Ok(_) => std::thread::park(),
|
||||||
Err(e) => {
|
Err(e) =>
|
||||||
|
{
|
||||||
let should_panic = if let windows_service::Error::Winapi(ref io_error) = e {
|
let should_panic = if let windows_service::Error::Winapi(ref io_error) = e {
|
||||||
io_error.raw_os_error() != Some(0x427) // ERROR_FAILED_SERVICE_CONTROLLER_CONNECT
|
io_error.raw_os_error() != Some(0x427) // ERROR_FAILED_SERVICE_CONTROLLER_CONNECT
|
||||||
} else {
|
} else { true };
|
||||||
true
|
|
||||||
};
|
|
||||||
|
|
||||||
if should_panic {
|
if should_panic {
|
||||||
panic!("SCM start an error: {}", e);
|
panic!("SCM start an error: {}", e);
|
||||||
|
|
|
@ -23,15 +23,9 @@ use tokio::{sync::broadcast, task::JoinSet};
|
||||||
|
|
||||||
pub type MyNodeInfo = crate::proto::web::MyNodeInfo;
|
pub type MyNodeInfo = crate::proto::web::MyNodeInfo;
|
||||||
|
|
||||||
#[derive(serde::Serialize, Clone)]
|
|
||||||
pub struct Event {
|
|
||||||
time: DateTime<Local>,
|
|
||||||
event: GlobalCtxEvent,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct EasyTierData {
|
struct EasyTierData {
|
||||||
events: RwLock<VecDeque<Event>>,
|
events: RwLock<VecDeque<(DateTime<Local>, GlobalCtxEvent)>>,
|
||||||
my_node_info: RwLock<MyNodeInfo>,
|
node_info: RwLock<MyNodeInfo>,
|
||||||
routes: RwLock<Vec<Route>>,
|
routes: RwLock<Vec<Route>>,
|
||||||
peers: RwLock<Vec<PeerInfo>>,
|
peers: RwLock<Vec<PeerInfo>>,
|
||||||
tun_fd: Arc<RwLock<Option<i32>>>,
|
tun_fd: Arc<RwLock<Option<i32>>>,
|
||||||
|
@ -46,7 +40,7 @@ impl Default for EasyTierData {
|
||||||
Self {
|
Self {
|
||||||
event_subscriber: RwLock::new(tx),
|
event_subscriber: RwLock::new(tx),
|
||||||
events: RwLock::new(VecDeque::new()),
|
events: RwLock::new(VecDeque::new()),
|
||||||
my_node_info: RwLock::new(MyNodeInfo::default()),
|
node_info: RwLock::new(MyNodeInfo::default()),
|
||||||
routes: RwLock::new(Vec::new()),
|
routes: RwLock::new(Vec::new()),
|
||||||
peers: RwLock::new(Vec::new()),
|
peers: RwLock::new(Vec::new()),
|
||||||
tun_fd: Arc::new(RwLock::new(None)),
|
tun_fd: Arc::new(RwLock::new(None)),
|
||||||
|
@ -85,12 +79,9 @@ impl EasyTierLauncher {
|
||||||
async fn handle_easytier_event(event: GlobalCtxEvent, data: &EasyTierData) {
|
async fn handle_easytier_event(event: GlobalCtxEvent, data: &EasyTierData) {
|
||||||
let mut events = data.events.write().unwrap();
|
let mut events = data.events.write().unwrap();
|
||||||
let _ = data.event_subscriber.read().unwrap().send(event.clone());
|
let _ = data.event_subscriber.read().unwrap().send(event.clone());
|
||||||
events.push_front(Event {
|
events.push_back((chrono::Local::now(), event));
|
||||||
time: chrono::Local::now(),
|
if events.len() > 100 {
|
||||||
event: event,
|
events.pop_front();
|
||||||
});
|
|
||||||
if events.len() > 20 {
|
|
||||||
events.pop_back();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,7 +153,7 @@ impl EasyTierLauncher {
|
||||||
global_ctx_c.get_flags().dev_name.clone();
|
global_ctx_c.get_flags().dev_name.clone();
|
||||||
|
|
||||||
let node_info = MyNodeInfo {
|
let node_info = MyNodeInfo {
|
||||||
virtual_ipv4: global_ctx_c.get_ipv4().map(|ip| ip.into()),
|
virtual_ipv4: global_ctx_c.get_ipv4().map(|x| x.address().into()),
|
||||||
hostname: global_ctx_c.get_hostname(),
|
hostname: global_ctx_c.get_hostname(),
|
||||||
version: EASYTIER_VERSION.to_string(),
|
version: EASYTIER_VERSION.to_string(),
|
||||||
ips: Some(global_ctx_c.get_ip_collector().collect_ip_addrs().await),
|
ips: Some(global_ctx_c.get_ip_collector().collect_ip_addrs().await),
|
||||||
|
@ -180,7 +171,7 @@ impl EasyTierLauncher {
|
||||||
.await,
|
.await,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
*data_c.my_node_info.write().unwrap() = node_info.clone();
|
*data_c.node_info.write().unwrap() = node_info.clone();
|
||||||
*data_c.routes.write().unwrap() = peer_mgr_c.list_routes().await;
|
*data_c.routes.write().unwrap() = peer_mgr_c.list_routes().await;
|
||||||
*data_c.peers.write().unwrap() = PeerManagerRpcService::new(peer_mgr_c.clone())
|
*data_c.peers.write().unwrap() = PeerManagerRpcService::new(peer_mgr_c.clone())
|
||||||
.list_peers()
|
.list_peers()
|
||||||
|
@ -276,13 +267,13 @@ impl EasyTierLauncher {
|
||||||
self.data.tun_dev_name.read().unwrap().clone()
|
self.data.tun_dev_name.read().unwrap().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_events(&self) -> Vec<Event> {
|
pub fn get_events(&self) -> Vec<(DateTime<Local>, GlobalCtxEvent)> {
|
||||||
let events = self.data.events.read().unwrap();
|
let events = self.data.events.read().unwrap();
|
||||||
events.iter().cloned().collect()
|
events.iter().cloned().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_node_info(&self) -> MyNodeInfo {
|
pub fn get_node_info(&self) -> MyNodeInfo {
|
||||||
self.data.my_node_info.read().unwrap().clone()
|
self.data.node_info.read().unwrap().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_routes(&self) -> Vec<Route> {
|
pub fn get_routes(&self) -> Vec<Route> {
|
||||||
|
@ -350,8 +341,9 @@ impl NetworkInstance {
|
||||||
events: launcher
|
events: launcher
|
||||||
.get_events()
|
.get_events()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|e| serde_json::to_string(e).unwrap())
|
.map(|(t, e)| (t.to_string(), format!("{:?}", e)))
|
||||||
.collect(),
|
.collect(),
|
||||||
|
node_info: Some(launcher.get_node_info()),
|
||||||
routes,
|
routes,
|
||||||
peers,
|
peers,
|
||||||
peer_route_pairs,
|
peer_route_pairs,
|
||||||
|
|
|
@ -43,7 +43,7 @@ message NetworkConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
message MyNodeInfo {
|
message MyNodeInfo {
|
||||||
common.Ipv4Inet virtual_ipv4 = 1;
|
common.Ipv4Addr virtual_ipv4 = 1;
|
||||||
string hostname = 2;
|
string hostname = 2;
|
||||||
string version = 3;
|
string version = 3;
|
||||||
peer_rpc.GetIpListResponse ips = 4;
|
peer_rpc.GetIpListResponse ips = 4;
|
||||||
|
@ -55,12 +55,13 @@ message MyNodeInfo {
|
||||||
message NetworkInstanceRunningInfo {
|
message NetworkInstanceRunningInfo {
|
||||||
string dev_name = 1;
|
string dev_name = 1;
|
||||||
MyNodeInfo my_node_info = 2;
|
MyNodeInfo my_node_info = 2;
|
||||||
repeated string events = 3;
|
map<string, string> events = 3;
|
||||||
repeated cli.Route routes = 4;
|
MyNodeInfo node_info = 4;
|
||||||
repeated cli.PeerInfo peers = 5;
|
repeated cli.Route routes = 5;
|
||||||
repeated cli.PeerRoutePair peer_route_pairs = 6;
|
repeated cli.PeerInfo peers = 6;
|
||||||
bool running = 7;
|
repeated cli.PeerRoutePair peer_route_pairs = 7;
|
||||||
optional string error_msg = 8;
|
bool running = 8;
|
||||||
|
optional string error_msg = 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
message NetworkInstanceRunningInfoMap {
|
message NetworkInstanceRunningInfoMap {
|
||||||
|
@ -96,7 +97,7 @@ message ValidateConfigResponse {
|
||||||
|
|
||||||
message RunNetworkInstanceRequest {
|
message RunNetworkInstanceRequest {
|
||||||
common.UUID inst_id = 1;
|
common.UUID inst_id = 1;
|
||||||
NetworkConfig config = 2;
|
string config = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message RunNetworkInstanceResponse {
|
message RunNetworkInstanceResponse {
|
||||||
|
|
|
@ -100,10 +100,7 @@ impl WebClientService for Controller {
|
||||||
_: BaseController,
|
_: BaseController,
|
||||||
req: RunNetworkInstanceRequest,
|
req: RunNetworkInstanceRequest,
|
||||||
) -> Result<RunNetworkInstanceResponse, rpc_types::error::Error> {
|
) -> Result<RunNetworkInstanceResponse, rpc_types::error::Error> {
|
||||||
if req.config.is_none() {
|
let cfg = TomlConfigLoader::new_from_str(&req.config)?;
|
||||||
return Err(anyhow::anyhow!("config is required").into());
|
|
||||||
}
|
|
||||||
let cfg = req.config.unwrap().gen_config()?;
|
|
||||||
let id = cfg.get_id();
|
let id = cfg.get_id();
|
||||||
if let Some(inst_id) = req.inst_id {
|
if let Some(inst_id) = req.inst_id {
|
||||||
cfg.set_id(inst_id.into());
|
cfg.set_id(inst_id.into());
|
||||||
|
|
861
pnpm-lock.yaml
861
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -5,7 +5,7 @@ Default permissions for the plugin
|
||||||
- `allow-ping`
|
- `allow-ping`
|
||||||
- `allow-start-vpn`
|
- `allow-start-vpn`
|
||||||
|
|
||||||
## Permission Table
|
### Permission Table
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
@ -295,59 +295,81 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
{
|
{
|
||||||
"description": "Enables the ping command without any pre-configured scope.",
|
"description": "allow-ping -> Enables the ping command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "allow-ping"
|
"enum": [
|
||||||
|
"allow-ping"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the ping command without any pre-configured scope.",
|
"description": "deny-ping -> Denies the ping command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "deny-ping"
|
"enum": [
|
||||||
|
"deny-ping"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the prepare_vpn command without any pre-configured scope.",
|
"description": "allow-prepare-vpn -> Enables the prepare_vpn command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "allow-prepare-vpn"
|
"enum": [
|
||||||
|
"allow-prepare-vpn"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the prepare_vpn command without any pre-configured scope.",
|
"description": "deny-prepare-vpn -> Denies the prepare_vpn command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "deny-prepare-vpn"
|
"enum": [
|
||||||
|
"deny-prepare-vpn"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the register_listener command without any pre-configured scope.",
|
"description": "allow-register-listener -> Enables the register_listener command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "allow-register-listener"
|
"enum": [
|
||||||
|
"allow-register-listener"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the register_listener command without any pre-configured scope.",
|
"description": "deny-register-listener -> Denies the register_listener command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "deny-register-listener"
|
"enum": [
|
||||||
|
"deny-register-listener"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the start_vpn command without any pre-configured scope.",
|
"description": "allow-start-vpn -> Enables the start_vpn command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "allow-start-vpn"
|
"enum": [
|
||||||
|
"allow-start-vpn"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the start_vpn command without any pre-configured scope.",
|
"description": "deny-start-vpn -> Denies the start_vpn command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "deny-start-vpn"
|
"enum": [
|
||||||
|
"deny-start-vpn"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the stop_vpn command without any pre-configured scope.",
|
"description": "allow-stop-vpn -> Enables the stop_vpn command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "allow-stop-vpn"
|
"enum": [
|
||||||
|
"allow-stop-vpn"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the stop_vpn command without any pre-configured scope.",
|
"description": "deny-stop-vpn -> Denies the stop_vpn command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "deny-stop-vpn"
|
"enum": [
|
||||||
|
"deny-stop-vpn"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Default permissions for the plugin",
|
"description": "default -> Default permissions for the plugin",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "default"
|
"enum": [
|
||||||
|
"default"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user