mirror of
https://github.com/EasyTier/EasyTier.git
synced 2025-10-02 07:22:23 +08:00
Compare commits
42 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
25ed41caf5 | ||
![]() |
4bb72b5606 | ||
![]() |
c4d8ea4fec | ||
![]() |
8588c9201a | ||
![]() |
dd2236c697 | ||
![]() |
bc7c4d8cd0 | ||
![]() |
aed54f7318 | ||
![]() |
86600c6315 | ||
![]() |
3f47f37470 | ||
![]() |
1324e6163e | ||
![]() |
89093167c6 | ||
![]() |
15ad92aef2 | ||
![]() |
6cdea38284 | ||
![]() |
9d455e22fa | ||
![]() |
4fc3ff8ce8 | ||
![]() |
88e6de9d7e | ||
![]() |
e948dbfcc1 | ||
![]() |
8aca5851f2 | ||
![]() |
18da94bf33 | ||
![]() |
1ac2e1c8e3 | ||
![]() |
a78b759741 | ||
![]() |
b5c3726e67 | ||
![]() |
efee3707da | ||
![]() |
bbd3453f36 | ||
![]() |
0bf42c53cc | ||
![]() |
2134bc9139 | ||
![]() |
4df8d7e976 | ||
![]() |
70708b34cc | ||
![]() |
949003ee1b | ||
![]() |
db9df1df94 | ||
![]() |
4dca25db86 | ||
![]() |
d87a440c04 | ||
![]() |
55efd62798 | ||
![]() |
70a41275c1 | ||
![]() |
dd941681ce | ||
![]() |
9824d0adaa | ||
![]() |
d2291628e0 | ||
![]() |
7ab8cad1af | ||
![]() |
2c017e0fc5 | ||
![]() |
d9453589ac | ||
![]() |
e344372616 | ||
![]() |
63821e56bc |
33
.github/workflows/core.yml
vendored
33
.github/workflows/core.yml
vendored
@@ -37,28 +37,28 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- TARGET: aarch64-unknown-linux-musl
|
- TARGET: aarch64-unknown-linux-musl
|
||||||
OS: ubuntu-latest
|
OS: ubuntu-22.04
|
||||||
ARTIFACT_NAME: linux-aarch64
|
ARTIFACT_NAME: linux-aarch64
|
||||||
- TARGET: x86_64-unknown-linux-musl
|
- TARGET: x86_64-unknown-linux-musl
|
||||||
OS: ubuntu-latest
|
OS: ubuntu-22.04
|
||||||
ARTIFACT_NAME: linux-x86_64
|
ARTIFACT_NAME: linux-x86_64
|
||||||
- TARGET: mips-unknown-linux-musl
|
- TARGET: mips-unknown-linux-musl
|
||||||
OS: ubuntu-latest
|
OS: ubuntu-22.04
|
||||||
ARTIFACT_NAME: linux-mips
|
ARTIFACT_NAME: linux-mips
|
||||||
- TARGET: mipsel-unknown-linux-musl
|
- TARGET: mipsel-unknown-linux-musl
|
||||||
OS: ubuntu-latest
|
OS: ubuntu-22.04
|
||||||
ARTIFACT_NAME: linux-mipsel
|
ARTIFACT_NAME: linux-mipsel
|
||||||
- TARGET: armv7-unknown-linux-musleabihf # raspberry pi 2-3-4, not tested
|
- TARGET: armv7-unknown-linux-musleabihf # raspberry pi 2-3-4, not tested
|
||||||
OS: ubuntu-latest
|
OS: ubuntu-22.04
|
||||||
ARTIFACT_NAME: linux-armv7hf
|
ARTIFACT_NAME: linux-armv7hf
|
||||||
- TARGET: armv7-unknown-linux-musleabi # raspberry pi 2-3-4, not tested
|
- TARGET: armv7-unknown-linux-musleabi # raspberry pi 2-3-4, not tested
|
||||||
OS: ubuntu-latest
|
OS: ubuntu-22.04
|
||||||
ARTIFACT_NAME: linux-armv7
|
ARTIFACT_NAME: linux-armv7
|
||||||
- TARGET: arm-unknown-linux-musleabihf # raspberry pi 0-1, not tested
|
- TARGET: arm-unknown-linux-musleabihf # raspberry pi 0-1, not tested
|
||||||
OS: ubuntu-latest
|
OS: ubuntu-22.04
|
||||||
ARTIFACT_NAME: linux-armhf
|
ARTIFACT_NAME: linux-armhf
|
||||||
- TARGET: arm-unknown-linux-musleabi # raspberry pi 0-1, not tested
|
- TARGET: arm-unknown-linux-musleabi # raspberry pi 0-1, not tested
|
||||||
OS: ubuntu-latest
|
OS: ubuntu-22.04
|
||||||
ARTIFACT_NAME: linux-arm
|
ARTIFACT_NAME: linux-arm
|
||||||
|
|
||||||
- TARGET: x86_64-apple-darwin
|
- TARGET: x86_64-apple-darwin
|
||||||
@@ -72,8 +72,12 @@ jobs:
|
|||||||
OS: windows-latest
|
OS: windows-latest
|
||||||
ARTIFACT_NAME: windows-x86_64
|
ARTIFACT_NAME: windows-x86_64
|
||||||
|
|
||||||
|
- TARGET: aarch64-pc-windows-msvc
|
||||||
|
OS: windows-latest
|
||||||
|
ARTIFACT_NAME: windows-arm64
|
||||||
|
|
||||||
- TARGET: x86_64-unknown-freebsd
|
- TARGET: x86_64-unknown-freebsd
|
||||||
OS: ubuntu-latest
|
OS: ubuntu-22.04
|
||||||
ARTIFACT_NAME: freebsd-13.2-x86_64
|
ARTIFACT_NAME: freebsd-13.2-x86_64
|
||||||
BSD_VERSION: 13.2
|
BSD_VERSION: 13.2
|
||||||
|
|
||||||
@@ -111,7 +115,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
bash ./.github/workflows/install_rust.sh
|
bash ./.github/workflows/install_rust.sh
|
||||||
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
|
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
|
||||||
cargo +nightly build -r --verbose --target $TARGET -Z build-std=std,panic_abort --no-default-features --features mips
|
cargo +nightly build -r --verbose --target $TARGET -Z build-std=std,panic_abort --no-default-features --features mips --package=easytier
|
||||||
else
|
else
|
||||||
cargo build --release --verbose --target $TARGET
|
cargo build --release --verbose --target $TARGET
|
||||||
fi
|
fi
|
||||||
@@ -164,10 +168,14 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
mkdir -p ./artifacts/objects/
|
mkdir -p ./artifacts/objects/
|
||||||
# windows is the only OS using a different convention for executable file name
|
# windows is the only OS using a different convention for executable file name
|
||||||
if [[ $OS =~ ^windows.*$ ]]; then
|
if [[ $OS =~ ^windows.*$ && $TARGET =~ ^x86_64.*$ ]]; then
|
||||||
SUFFIX=.exe
|
SUFFIX=.exe
|
||||||
cp easytier/third_party/Packet.dll ./artifacts/objects/
|
cp easytier/third_party/Packet.dll ./artifacts/objects/
|
||||||
cp easytier/third_party/wintun.dll ./artifacts/objects/
|
cp easytier/third_party/wintun.dll ./artifacts/objects/
|
||||||
|
elif [[ $OS =~ ^windows.*$ && $TARGET =~ ^aarch64.*$ ]]; then
|
||||||
|
SUFFIX=.exe
|
||||||
|
cp easytier/third_party/arm64/Packet.dll ./artifacts/objects/
|
||||||
|
cp easytier/third_party/arm64/wintun.dll ./artifacts/objects/
|
||||||
fi
|
fi
|
||||||
if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then
|
if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then
|
||||||
TAG=$GITHUB_REF_NAME
|
TAG=$GITHUB_REF_NAME
|
||||||
@@ -182,6 +190,9 @@ jobs:
|
|||||||
|
|
||||||
mv ./target/$TARGET/release/easytier-core"$SUFFIX" ./artifacts/objects/
|
mv ./target/$TARGET/release/easytier-core"$SUFFIX" ./artifacts/objects/
|
||||||
mv ./target/$TARGET/release/easytier-cli"$SUFFIX" ./artifacts/objects/
|
mv ./target/$TARGET/release/easytier-cli"$SUFFIX" ./artifacts/objects/
|
||||||
|
if [[ ! $TARGET =~ ^mips.*$ ]]; then
|
||||||
|
mv ./target/$TARGET/release/easytier-web"$SUFFIX" ./artifacts/objects/
|
||||||
|
fi
|
||||||
|
|
||||||
mv ./artifacts/objects/* ./artifacts/
|
mv ./artifacts/objects/* ./artifacts/
|
||||||
rm -rf ./artifacts/objects/
|
rm -rf ./artifacts/objects/
|
||||||
|
68
.github/workflows/gui.yml
vendored
68
.github/workflows/gui.yml
vendored
@@ -36,11 +36,11 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- TARGET: aarch64-unknown-linux-musl
|
- TARGET: aarch64-unknown-linux-musl
|
||||||
OS: ubuntu-latest
|
OS: ubuntu-22.04
|
||||||
GUI_TARGET: aarch64-unknown-linux-gnu
|
GUI_TARGET: aarch64-unknown-linux-gnu
|
||||||
ARTIFACT_NAME: linux-aarch64
|
ARTIFACT_NAME: linux-aarch64
|
||||||
- TARGET: x86_64-unknown-linux-musl
|
- TARGET: x86_64-unknown-linux-musl
|
||||||
OS: ubuntu-latest
|
OS: ubuntu-22.04
|
||||||
GUI_TARGET: x86_64-unknown-linux-gnu
|
GUI_TARGET: x86_64-unknown-linux-gnu
|
||||||
ARTIFACT_NAME: linux-x86_64
|
ARTIFACT_NAME: linux-x86_64
|
||||||
|
|
||||||
@@ -58,6 +58,11 @@ jobs:
|
|||||||
GUI_TARGET: x86_64-pc-windows-msvc
|
GUI_TARGET: x86_64-pc-windows-msvc
|
||||||
ARTIFACT_NAME: windows-x86_64
|
ARTIFACT_NAME: windows-x86_64
|
||||||
|
|
||||||
|
- TARGET: aarch64-pc-windows-msvc
|
||||||
|
OS: windows-latest
|
||||||
|
GUI_TARGET: aarch64-pc-windows-msvc
|
||||||
|
ARTIFACT_NAME: windows-arm64
|
||||||
|
|
||||||
runs-on: ${{ matrix.OS }}
|
runs-on: ${{ matrix.OS }}
|
||||||
env:
|
env:
|
||||||
NAME: easytier
|
NAME: easytier
|
||||||
@@ -99,8 +104,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: |
|
run: |
|
||||||
(cd easytier-gui; pnpm install)
|
pnpm -r install
|
||||||
(cd tauri-plugin-vpnservice; pnpm install; pnpm build)
|
pnpm -r build
|
||||||
|
|
||||||
- name: Cargo cache
|
- name: Cargo cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -123,41 +128,52 @@ jobs:
|
|||||||
if: ${{ matrix.TARGET == 'aarch64-unknown-linux-musl' }}
|
if: ${{ matrix.TARGET == 'aarch64-unknown-linux-musl' }}
|
||||||
run: |
|
run: |
|
||||||
# see https://tauri.app/v1/guides/building/linux/
|
# see https://tauri.app/v1/guides/building/linux/
|
||||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ noble main restricted" | sudo tee /etc/apt/sources.list
|
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy main restricted" | sudo tee /etc/apt/sources.list
|
||||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ noble-updates main restricted" | sudo tee -a /etc/apt/sources.list
|
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates main restricted" | sudo tee -a /etc/apt/sources.list
|
||||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ noble universe" | sudo tee -a /etc/apt/sources.list
|
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy universe" | sudo tee -a /etc/apt/sources.list
|
||||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ noble-updates universe" | sudo tee -a /etc/apt/sources.list
|
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates universe" | sudo tee -a /etc/apt/sources.list
|
||||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ noble multiverse" | sudo tee -a /etc/apt/sources.list
|
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy multiverse" | sudo tee -a /etc/apt/sources.list
|
||||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ noble-updates multiverse" | sudo tee -a /etc/apt/sources.list
|
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates multiverse" | sudo tee -a /etc/apt/sources.list
|
||||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||||
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ noble-security main restricted" | sudo tee -a /etc/apt/sources.list
|
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security main restricted" | sudo tee -a /etc/apt/sources.list
|
||||||
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ noble-security universe" | sudo tee -a /etc/apt/sources.list
|
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security universe" | sudo tee -a /etc/apt/sources.list
|
||||||
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ noble-security multiverse" | sudo tee -a /etc/apt/sources.list
|
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security multiverse" | sudo tee -a /etc/apt/sources.list
|
||||||
|
|
||||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble main restricted" | sudo tee -a /etc/apt/sources.list
|
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy main restricted" | sudo tee -a /etc/apt/sources.list
|
||||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted" | sudo tee -a /etc/apt/sources.list
|
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main restricted" | sudo tee -a /etc/apt/sources.list
|
||||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble universe" | sudo tee -a /etc/apt/sources.list
|
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy universe" | sudo tee -a /etc/apt/sources.list
|
||||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble-updates universe" | sudo tee -a /etc/apt/sources.list
|
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates universe" | sudo tee -a /etc/apt/sources.list
|
||||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble multiverse" | sudo tee -a /etc/apt/sources.list
|
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy multiverse" | sudo tee -a /etc/apt/sources.list
|
||||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble-updates multiverse" | sudo tee -a /etc/apt/sources.list
|
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates multiverse" | sudo tee -a /etc/apt/sources.list
|
||||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted" | sudo tee -a /etc/apt/sources.list
|
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main restricted" | sudo tee -a /etc/apt/sources.list
|
||||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble-security universe" | sudo tee -a /etc/apt/sources.list
|
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security universe" | sudo tee -a /etc/apt/sources.list
|
||||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble-security multiverse" | sudo tee -a /etc/apt/sources.list
|
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security multiverse" | sudo tee -a /etc/apt/sources.list
|
||||||
|
|
||||||
sudo dpkg --add-architecture arm64
|
sudo dpkg --add-architecture arm64
|
||||||
sudo apt-get update && sudo apt-get upgrade -y
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libgstreamer1.0-0:arm64 gstreamer1.0-plugins-base:arm64 gstreamer1.0-plugins-good:arm64
|
||||||
|
sudo apt-get install -y libgstreamer-gl1.0-0:arm64 libgstreamer-plugins-base1.0-0:arm64 libgstreamer-plugins-good1.0-0:arm64 libwebkit2gtk-4.1-0:arm64
|
||||||
sudo apt install -f -o Dpkg::Options::="--force-overwrite" libwebkit2gtk-4.1-dev:arm64 libssl-dev:arm64 gcc-aarch64-linux-gnu
|
sudo apt install -f -o Dpkg::Options::="--force-overwrite" libwebkit2gtk-4.1-dev:arm64 libssl-dev:arm64 gcc-aarch64-linux-gnu
|
||||||
echo "PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/" >> "$GITHUB_ENV"
|
echo "PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/" >> "$GITHUB_ENV"
|
||||||
echo "PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/" >> "$GITHUB_ENV"
|
echo "PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: copy correct DLLs
|
||||||
|
if: ${{ matrix.OS == 'windows-latest' }}
|
||||||
|
run: |
|
||||||
|
if [[ $GUI_TARGET =~ ^aarch64.*$ ]]; then
|
||||||
|
cp ./easytier/third_party/arm64/*.dll ./easytier-gui/src-tauri/
|
||||||
|
else
|
||||||
|
cp ./easytier/third_party/*.dll ./easytier-gui/src-tauri/
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Build GUI
|
- name: Build GUI
|
||||||
if: ${{ matrix.GUI_TARGET != '' }}
|
if: ${{ matrix.GUI_TARGET != '' }}
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
with:
|
with:
|
||||||
projectPath: ./easytier-gui
|
projectPath: ./easytier-gui
|
||||||
# https://tauri.app/v1/guides/building/linux/#cross-compiling-tauri-applications-for-arm-based-devices
|
# https://tauri.app/v1/guides/building/linux/#cross-compiling-tauri-applications-for-arm-based-devices
|
||||||
args: --verbose --target ${{ matrix.GUI_TARGET }} ${{ matrix.OS == 'ubuntu-latest' && contains(matrix.TARGET, 'aarch64') && '--bundles deb' || '' }}
|
args: --verbose --target ${{ matrix.GUI_TARGET }} ${{ matrix.OS == 'ubuntu-22.04' && contains(matrix.TARGET, 'aarch64') && '--bundles deb' || '' }}
|
||||||
|
|
||||||
- name: Compress
|
- name: Compress
|
||||||
run: |
|
run: |
|
||||||
|
6
.github/workflows/mobile.yml
vendored
6
.github/workflows/mobile.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- TARGET: android
|
- TARGET: android
|
||||||
OS: ubuntu-latest
|
OS: ubuntu-22.04
|
||||||
ARTIFACT_NAME: android
|
ARTIFACT_NAME: android
|
||||||
runs-on: ${{ matrix.OS }}
|
runs-on: ${{ matrix.OS }}
|
||||||
env:
|
env:
|
||||||
@@ -95,8 +95,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: |
|
run: |
|
||||||
(cd easytier-gui; pnpm install)
|
pnpm -r install
|
||||||
(cd tauri-plugin-vpnservice; pnpm install; pnpm build)
|
pnpm -r build
|
||||||
|
|
||||||
- name: Cargo cache
|
- name: Cargo cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -21,7 +21,7 @@ on:
|
|||||||
version:
|
version:
|
||||||
description: 'Version for this release'
|
description: 'Version for this release'
|
||||||
type: string
|
type: string
|
||||||
default: 'v2.0.1'
|
default: 'v2.1.0'
|
||||||
required: true
|
required: true
|
||||||
make_latest:
|
make_latest:
|
||||||
description: 'Mark this release as latest'
|
description: 'Mark this release as latest'
|
||||||
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
|||||||
skip_after_successful_duplicate: 'true'
|
skip_after_successful_duplicate: 'true'
|
||||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/test.yml"]'
|
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/test.yml"]'
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
needs: pre_job
|
needs: pre_job
|
||||||
if: needs.pre_job.outputs.should_skip != 'true'
|
if: needs.pre_job.outputs.should_skip != 'true'
|
||||||
steps:
|
steps:
|
||||||
|
8
.gitignore
vendored
8
.gitignore
vendored
@@ -11,6 +11,7 @@ target-*/
|
|||||||
*.pdb
|
*.pdb
|
||||||
|
|
||||||
.vscode
|
.vscode
|
||||||
|
/.idea
|
||||||
|
|
||||||
# perf & flamegraph
|
# perf & flamegraph
|
||||||
perf.data
|
perf.data
|
||||||
@@ -29,3 +30,10 @@ musl_gcc
|
|||||||
|
|
||||||
# log
|
# log
|
||||||
easytier-panic.log
|
easytier-panic.log
|
||||||
|
|
||||||
|
# web
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
.vite
|
||||||
|
|
||||||
|
easytier-gui/src-tauri/*.dll
|
||||||
|
2470
Cargo.lock
generated
2470
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = ["easytier", "easytier-gui/src-tauri"]
|
members = ["easytier", "easytier-gui/src-tauri", "easytier-rpc-build", "easytier-web"]
|
||||||
default-members = ["easytier"]
|
default-members = ["easytier", "easytier-web"]
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
panic = "unwind"
|
panic = "unwind"
|
||||||
|
@@ -4,84 +4,27 @@
|
|||||||
"path": "."
|
"path": "."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"name": "gui",
|
||||||
"path": "easytier-gui"
|
"path": "easytier-gui"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"name": "core",
|
||||||
"path": "easytier"
|
"path": "easytier"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vpnservice",
|
||||||
|
"path": "tauri-plugin-vpnservice"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rpc-build",
|
||||||
|
"path": "easytier-rpc-build"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
"eslint.useFlatConfig": true,
|
"i18n-ally.sourceLanguage": "cn",
|
||||||
|
"i18n-ally.keystyle": "nested",
|
||||||
|
"i18n-ally.sortKeys": true,
|
||||||
|
// Disable the default formatter
|
||||||
"prettier.enable": false,
|
"prettier.enable": false,
|
||||||
"editor.formatOnSave": false,
|
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"source.fixAll.eslint": "explicit",
|
|
||||||
"source.organizeImports": "never"
|
|
||||||
},
|
|
||||||
"eslint.rules.customizations": [
|
|
||||||
{
|
|
||||||
"rule": "style/*",
|
|
||||||
"severity": "off"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "style/eol-last",
|
|
||||||
"severity": "error"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "format/*",
|
|
||||||
"severity": "off"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "*-indent",
|
|
||||||
"severity": "off"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "*-spacing",
|
|
||||||
"severity": "off"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "*-spaces",
|
|
||||||
"severity": "off"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "*-order",
|
|
||||||
"severity": "off"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "*-dangle",
|
|
||||||
"severity": "off"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "*-newline",
|
|
||||||
"severity": "off"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "*quotes",
|
|
||||||
"severity": "off"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "*semi",
|
|
||||||
"severity": "off"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"eslint.validate": [
|
|
||||||
"code-workspace",
|
|
||||||
"javascript",
|
|
||||||
"javascriptreact",
|
|
||||||
"typescript",
|
|
||||||
"typescriptreact",
|
|
||||||
"vue",
|
|
||||||
"html",
|
|
||||||
"markdown",
|
|
||||||
"json",
|
|
||||||
"jsonc",
|
|
||||||
"yaml",
|
|
||||||
"toml",
|
|
||||||
"gql",
|
|
||||||
"graphql"
|
|
||||||
],
|
|
||||||
"i18n-ally.localesPaths": [
|
|
||||||
"easytier-gui/locales"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,2 +0,0 @@
|
|||||||
shamefully-hoist=true
|
|
||||||
strict-peer-dependencies=false
|
|
82
easytier-gui/.vscode/settings.json
vendored
82
easytier-gui/.vscode/settings.json
vendored
@@ -1,5 +1,81 @@
|
|||||||
{
|
{
|
||||||
"i18n-ally.localesPaths": [
|
"cSpell.words": [
|
||||||
"locales"
|
"easytier",
|
||||||
|
"Vite",
|
||||||
|
"vueuse",
|
||||||
|
"pinia",
|
||||||
|
"demi",
|
||||||
|
"antfu",
|
||||||
|
"iconify",
|
||||||
|
"intlify",
|
||||||
|
"vitejs",
|
||||||
|
"unplugin",
|
||||||
|
"pnpm"
|
||||||
|
],
|
||||||
|
"i18n-ally.localesPaths": "locales",
|
||||||
|
"editor.formatOnSave": false,
|
||||||
|
// Auto fix
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit",
|
||||||
|
"source.organizeImports": "never"
|
||||||
|
},
|
||||||
|
// Silent the stylistic rules in you IDE, but still auto fix them
|
||||||
|
"eslint.rules.customizations": [
|
||||||
|
{
|
||||||
|
"rule": "style/*",
|
||||||
|
"severity": "off"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "format/*",
|
||||||
|
"severity": "off"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "*-indent",
|
||||||
|
"severity": "off"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "*-spacing",
|
||||||
|
"severity": "off"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "*-spaces",
|
||||||
|
"severity": "off"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "*-order",
|
||||||
|
"severity": "off"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "*-dangle",
|
||||||
|
"severity": "off"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "*-newline",
|
||||||
|
"severity": "off"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "*quotes",
|
||||||
|
"severity": "off"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "*semi",
|
||||||
|
"severity": "off"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
// The following is optional.
|
||||||
|
// It's better to put under project setting `.vscode/settings.json`
|
||||||
|
// to avoid conflicts with working with different eslint configs
|
||||||
|
// that does not support all formats.
|
||||||
|
"eslint.validate": [
|
||||||
|
"javascript",
|
||||||
|
"javascriptreact",
|
||||||
|
"typescript",
|
||||||
|
"typescriptreact",
|
||||||
|
"vue",
|
||||||
|
"html",
|
||||||
|
"markdown",
|
||||||
|
"json",
|
||||||
|
"jsonc",
|
||||||
|
"yaml"
|
||||||
]
|
]
|
||||||
}
|
}
|
@@ -72,6 +72,8 @@ loss_rate: 丢包率
|
|||||||
status:
|
status:
|
||||||
version: 内核版本
|
version: 内核版本
|
||||||
local: 本机
|
local: 本机
|
||||||
|
server: 服务器
|
||||||
|
relay: 中继
|
||||||
|
|
||||||
run_network: 运行网络
|
run_network: 运行网络
|
||||||
stop_network: 停止网络
|
stop_network: 停止网络
|
||||||
@@ -91,3 +93,23 @@ about:
|
|||||||
license: 许可证
|
license: 许可证
|
||||||
description: 一个简单、安全、去中心化的内网穿透 VPN 组网方案,使用 Rust 语言和 Tokio 框架实现。
|
description: 一个简单、安全、去中心化的内网穿透 VPN 组网方案,使用 Rust 语言和 Tokio 框架实现。
|
||||||
check_update: 检查更新
|
check_update: 检查更新
|
||||||
|
|
||||||
|
event:
|
||||||
|
Unknown: 未知
|
||||||
|
TunDeviceReady: Tun设备就绪
|
||||||
|
TunDeviceError: Tun设备错误
|
||||||
|
PeerAdded: 对端添加
|
||||||
|
PeerRemoved: 对端移除
|
||||||
|
PeerConnAdded: 对端连接添加
|
||||||
|
PeerConnRemoved: 对端连接移除
|
||||||
|
ListenerAdded: 监听器添加
|
||||||
|
ListenerAddFailed: 监听器添加失败
|
||||||
|
ListenerAcceptFailed: 监听器接受连接失败
|
||||||
|
ConnectionAccepted: 连接已接受
|
||||||
|
ConnectionError: 连接错误
|
||||||
|
Connecting: 正在连接
|
||||||
|
ConnectError: 连接错误
|
||||||
|
VpnPortalClientConnected: VPN门户客户端已连接
|
||||||
|
VpnPortalClientDisconnected: VPN门户客户端已断开连接
|
||||||
|
DhcpIpv4Changed: DHCP IPv4地址更改
|
||||||
|
DhcpIpv4Conflicted: DHCP IPv4地址冲突
|
||||||
|
@@ -71,6 +71,8 @@ loss_rate: Loss Rate
|
|||||||
status:
|
status:
|
||||||
version: Version
|
version: Version
|
||||||
local: Local
|
local: Local
|
||||||
|
server: Server
|
||||||
|
relay: Relay
|
||||||
|
|
||||||
run_network: Run Network
|
run_network: Run Network
|
||||||
stop_network: Stop Network
|
stop_network: Stop Network
|
||||||
@@ -90,3 +92,23 @@ about:
|
|||||||
license: License
|
license: License
|
||||||
description: 'EasyTier is a simple, safe and decentralized VPN networking solution implemented with the Rust language and Tokio framework.'
|
description: 'EasyTier is a simple, safe and decentralized VPN networking solution implemented with the Rust language and Tokio framework.'
|
||||||
check_update: Check Update
|
check_update: Check Update
|
||||||
|
|
||||||
|
event:
|
||||||
|
Unknown: Unknown
|
||||||
|
TunDeviceReady: TunDeviceReady
|
||||||
|
TunDeviceError: TunDeviceError
|
||||||
|
PeerAdded: PeerAdded
|
||||||
|
PeerRemoved: PeerRemoved
|
||||||
|
PeerConnAdded: PeerConnAdded
|
||||||
|
PeerConnRemoved: PeerConnRemoved
|
||||||
|
ListenerAdded: ListenerAdded
|
||||||
|
ListenerAddFailed: ListenerAddFailed
|
||||||
|
ListenerAcceptFailed: ListenerAcceptFailed
|
||||||
|
ConnectionAccepted: ConnectionAccepted
|
||||||
|
ConnectionError: ConnectionError
|
||||||
|
Connecting: Connecting
|
||||||
|
ConnectError: ConnectError
|
||||||
|
VpnPortalClientConnected: VpnPortalClientConnected
|
||||||
|
VpnPortalClientDisconnected: VpnPortalClientDisconnected
|
||||||
|
DhcpIpv4Changed: DhcpIpv4Changed
|
||||||
|
DhcpIpv4Conflicted: DhcpIpv4Conflicted
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "easytier-gui",
|
"name": "easytier-gui",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "2.0.1",
|
"version": "2.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vue-tsc --noEmit && vite build",
|
"build": "vue-tsc --noEmit && vite build",
|
||||||
@@ -12,33 +13,32 @@
|
|||||||
"lint:fix": "eslint . --ignore-pattern src-tauri --fix"
|
"lint:fix": "eslint . --ignore-pattern src-tauri --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@primevue/themes": "^4.1.0",
|
"@primevue/themes": "^4.2.1",
|
||||||
"@tauri-apps/plugin-autostart": "2.0.0-rc.1",
|
"@tauri-apps/plugin-autostart": "2.0.0",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "2.0.0-rc.1",
|
"@tauri-apps/plugin-clipboard-manager": "2.0.0",
|
||||||
"@tauri-apps/plugin-os": "2.0.0-rc.1",
|
"@tauri-apps/plugin-os": "2.0.0",
|
||||||
"@tauri-apps/plugin-process": "2.0.0-rc.1",
|
"@tauri-apps/plugin-process": "2.0.0",
|
||||||
"@tauri-apps/plugin-shell": "2.0.0-rc.1",
|
"@tauri-apps/plugin-shell": "2.0.1",
|
||||||
|
"@vueuse/core": "^11.2.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",
|
||||||
"primeflex": "^3.3.1",
|
"primevue": "^4.2.1",
|
||||||
"primeicons": "^7.0.0",
|
"tauri-plugin-vpnservice-api": "workspace:*",
|
||||||
"primevue": "^4.1.0",
|
"vue": "^3.5.12",
|
||||||
"tauri-plugin-vpnservice-api": "link:..\\tauri-plugin-vpnservice",
|
|
||||||
"vue": "^3.5.11",
|
|
||||||
"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.0.0-rc.0",
|
"@tauri-apps/api": "2.1.0",
|
||||||
"@tauri-apps/cli": "2.0.0-rc.3",
|
"@tauri-apps/cli": "2.1.0",
|
||||||
"@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.29.1",
|
"@vue-macros/volar": "0.30.5",
|
||||||
"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 +48,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.12.3",
|
"unplugin-vue-macros": "^2.13.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 +56,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.6"
|
"vue-tsc": "^2.1.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1255
easytier-gui/pnpm-lock.yaml
generated
1255
easytier-gui/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "easytier-gui"
|
name = "easytier-gui"
|
||||||
version = "2.0.1"
|
version = "2.1.0"
|
||||||
description = "EasyTier GUI"
|
description = "EasyTier GUI"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@@ -15,10 +15,11 @@ 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.0.0-rc", features = [
|
tauri = { version = "2.1", features = [
|
||||||
"tray-icon",
|
"tray-icon",
|
||||||
"image-png",
|
"image-png",
|
||||||
"image-ico",
|
"image-ico",
|
||||||
|
"devtools",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
@@ -37,13 +38,13 @@ gethostname = "0.5"
|
|||||||
|
|
||||||
dunce = "1.0.4"
|
dunce = "1.0.4"
|
||||||
|
|
||||||
tauri-plugin-shell = "2.0.0-rc"
|
tauri-plugin-shell = "2.0"
|
||||||
tauri-plugin-process = "2.0.0-rc"
|
tauri-plugin-process = "2.0"
|
||||||
tauri-plugin-clipboard-manager = "2.0.0-rc"
|
tauri-plugin-clipboard-manager = "2.0"
|
||||||
tauri-plugin-positioner = { version = "2.0.0-rc", features = ["tray-icon"] }
|
tauri-plugin-positioner = { version = "2.0", features = ["tray-icon"] }
|
||||||
tauri-plugin-vpnservice = { path = "../../tauri-plugin-vpnservice" }
|
tauri-plugin-vpnservice = { path = "../../tauri-plugin-vpnservice" }
|
||||||
tauri-plugin-os = "2.0.0-rc"
|
tauri-plugin-os = "2.0"
|
||||||
tauri-plugin-autostart = "2.0.0-rc"
|
tauri-plugin-autostart = "2.0"
|
||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
Binary file not shown.
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
"identifier": "migrated",
|
"identifier": "migrated",
|
||||||
"description": "permissions that were migrated from v1",
|
"description": "permissions that were migrated from v1",
|
||||||
"local": true,
|
"local": true,
|
||||||
@@ -13,6 +14,7 @@
|
|||||||
"core:window:allow-show",
|
"core:window:allow-show",
|
||||||
"core:window:allow-hide",
|
"core:window:allow-hide",
|
||||||
"core:window:allow-set-focus",
|
"core:window:allow-set-focus",
|
||||||
|
"core:window:allow-set-title",
|
||||||
"core:app:default",
|
"core:app:default",
|
||||||
"core:resources:default",
|
"core:resources:default",
|
||||||
"core:menu:default",
|
"core:menu:default",
|
||||||
@@ -24,7 +26,6 @@
|
|||||||
"shell:default",
|
"shell:default",
|
||||||
"process:default",
|
"process:default",
|
||||||
"clipboard-manager:default",
|
"clipboard-manager:default",
|
||||||
"core:tray:default",
|
|
||||||
"core:tray:allow-new",
|
"core:tray:allow-new",
|
||||||
"core:tray:allow-set-menu",
|
"core:tray:allow-set-menu",
|
||||||
"core:tray:allow-set-title",
|
"core:tray:allow-set-title",
|
||||||
|
@@ -3,174 +3,20 @@
|
|||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use anyhow::Context;
|
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use easytier::{
|
use easytier::{
|
||||||
common::config::{
|
common::config::{ConfigLoader, FileLoggerConfig, TomlConfigLoader},
|
||||||
ConfigLoader, FileLoggerConfig, Flags, NetworkIdentity, PeerConfig, TomlConfigLoader,
|
launcher::{NetworkConfig, NetworkInstance, NetworkInstanceRunningInfo},
|
||||||
VpnPortalConfig,
|
|
||||||
},
|
|
||||||
launcher::{NetworkInstance, NetworkInstanceRunningInfo},
|
|
||||||
utils::{self, NewFilterSender},
|
utils::{self, NewFilterSender},
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use tauri::Manager as _;
|
use tauri::Manager as _;
|
||||||
|
|
||||||
pub const AUTOSTART_ARG: &str = "--autostart";
|
pub const AUTOSTART_ARG: &str = "--autostart";
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
||||||
enum NetworkingMethod {
|
|
||||||
PublicServer,
|
|
||||||
Manual,
|
|
||||||
Standalone,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for NetworkingMethod {
|
|
||||||
fn default() -> Self {
|
|
||||||
NetworkingMethod::PublicServer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
|
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, Default)]
|
|
||||||
struct NetworkConfig {
|
|
||||||
instance_id: String,
|
|
||||||
|
|
||||||
dhcp: bool,
|
|
||||||
virtual_ipv4: String,
|
|
||||||
hostname: Option<String>,
|
|
||||||
network_name: String,
|
|
||||||
network_secret: String,
|
|
||||||
networking_method: NetworkingMethod,
|
|
||||||
|
|
||||||
public_server_url: String,
|
|
||||||
peer_urls: Vec<String>,
|
|
||||||
|
|
||||||
proxy_cidrs: Vec<String>,
|
|
||||||
|
|
||||||
enable_vpn_portal: bool,
|
|
||||||
vpn_portal_listen_port: i32,
|
|
||||||
vpn_portal_client_network_addr: String,
|
|
||||||
vpn_portal_client_network_len: i32,
|
|
||||||
|
|
||||||
advanced_settings: bool,
|
|
||||||
|
|
||||||
listener_urls: Vec<String>,
|
|
||||||
rpc_port: i32,
|
|
||||||
latency_first: bool,
|
|
||||||
|
|
||||||
dev_name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NetworkConfig {
|
|
||||||
fn gen_config(&self) -> Result<TomlConfigLoader, anyhow::Error> {
|
|
||||||
let cfg = TomlConfigLoader::default();
|
|
||||||
cfg.set_id(
|
|
||||||
self.instance_id
|
|
||||||
.parse()
|
|
||||||
.with_context(|| format!("failed to parse instance id: {}", self.instance_id))?,
|
|
||||||
);
|
|
||||||
cfg.set_hostname(self.hostname.clone());
|
|
||||||
cfg.set_dhcp(self.dhcp);
|
|
||||||
cfg.set_inst_name(self.network_name.clone());
|
|
||||||
cfg.set_network_identity(NetworkIdentity::new(
|
|
||||||
self.network_name.clone(),
|
|
||||||
self.network_secret.clone(),
|
|
||||||
));
|
|
||||||
|
|
||||||
if !self.dhcp {
|
|
||||||
if self.virtual_ipv4.len() > 0 {
|
|
||||||
cfg.set_ipv4(Some(self.virtual_ipv4.parse().with_context(|| {
|
|
||||||
format!("failed to parse ipv4 address: {}", self.virtual_ipv4)
|
|
||||||
})?))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match self.networking_method {
|
|
||||||
NetworkingMethod::PublicServer => {
|
|
||||||
cfg.set_peers(vec![PeerConfig {
|
|
||||||
uri: self.public_server_url.parse().with_context(|| {
|
|
||||||
format!(
|
|
||||||
"failed to parse public server uri: {}",
|
|
||||||
self.public_server_url
|
|
||||||
)
|
|
||||||
})?,
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
NetworkingMethod::Manual => {
|
|
||||||
let mut peers = vec![];
|
|
||||||
for peer_url in self.peer_urls.iter() {
|
|
||||||
if peer_url.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
peers.push(PeerConfig {
|
|
||||||
uri: peer_url
|
|
||||||
.parse()
|
|
||||||
.with_context(|| format!("failed to parse peer uri: {}", peer_url))?,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.set_peers(peers);
|
|
||||||
}
|
|
||||||
NetworkingMethod::Standalone => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut listener_urls = vec![];
|
|
||||||
for listener_url in self.listener_urls.iter() {
|
|
||||||
if listener_url.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
listener_urls.push(
|
|
||||||
listener_url
|
|
||||||
.parse()
|
|
||||||
.with_context(|| format!("failed to parse listener uri: {}", listener_url))?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
cfg.set_listeners(listener_urls);
|
|
||||||
|
|
||||||
for n in self.proxy_cidrs.iter() {
|
|
||||||
cfg.add_proxy_cidr(
|
|
||||||
n.parse()
|
|
||||||
.with_context(|| format!("failed to parse proxy network: {}", n))?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.set_rpc_portal(
|
|
||||||
format!("0.0.0.0:{}", self.rpc_port)
|
|
||||||
.parse()
|
|
||||||
.with_context(|| format!("failed to parse rpc portal port: {}", self.rpc_port))?,
|
|
||||||
);
|
|
||||||
|
|
||||||
if self.enable_vpn_portal {
|
|
||||||
let cidr = format!(
|
|
||||||
"{}/{}",
|
|
||||||
self.vpn_portal_client_network_addr, self.vpn_portal_client_network_len
|
|
||||||
);
|
|
||||||
cfg.set_vpn_portal_config(VpnPortalConfig {
|
|
||||||
client_cidr: cidr
|
|
||||||
.parse()
|
|
||||||
.with_context(|| format!("failed to parse vpn portal client cidr: {}", cidr))?,
|
|
||||||
wireguard_listen: format!("0.0.0.0:{}", self.vpn_portal_listen_port)
|
|
||||||
.parse()
|
|
||||||
.with_context(|| {
|
|
||||||
format!(
|
|
||||||
"failed to parse vpn portal wireguard listen port. {}",
|
|
||||||
self.vpn_portal_listen_port
|
|
||||||
)
|
|
||||||
})?,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let mut flags = Flags::default();
|
|
||||||
flags.latency_first = self.latency_first;
|
|
||||||
flags.dev_name = self.dev_name.clone();
|
|
||||||
cfg.set_flags(flags);
|
|
||||||
Ok(cfg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static INSTANCE_MAP: once_cell::sync::Lazy<DashMap<String, NetworkInstance>> =
|
static INSTANCE_MAP: once_cell::sync::Lazy<DashMap<String, NetworkInstance>> =
|
||||||
once_cell::sync::Lazy::new(DashMap::new);
|
once_cell::sync::Lazy::new(DashMap::new);
|
||||||
|
|
||||||
@@ -198,10 +44,10 @@ fn parse_network_config(cfg: NetworkConfig) -> Result<String, String> {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn run_network_instance(cfg: NetworkConfig) -> Result<(), String> {
|
fn run_network_instance(cfg: NetworkConfig) -> Result<(), String> {
|
||||||
if INSTANCE_MAP.contains_key(&cfg.instance_id) {
|
if INSTANCE_MAP.contains_key(cfg.instance_id()) {
|
||||||
return Err("instance already exists".to_string());
|
return Err("instance already exists".to_string());
|
||||||
}
|
}
|
||||||
let instance_id = cfg.instance_id.clone();
|
let instance_id = cfg.instance_id().to_string();
|
||||||
|
|
||||||
let cfg = cfg.gen_config().map_err(|e| e.to_string())?;
|
let cfg = cfg.gen_config().map_err(|e| e.to_string())?;
|
||||||
let mut instance = NetworkInstance::new(cfg);
|
let mut instance = NetworkInstance::new(cfg);
|
||||||
@@ -328,7 +174,7 @@ pub fn run() {
|
|||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.plugin(tauri_plugin_vpnservice::init());
|
.plugin(tauri_plugin_vpnservice::init());
|
||||||
|
|
||||||
builder
|
let app = builder
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
// for logging config
|
// for logging config
|
||||||
let Ok(log_dir) = app.path().app_log_dir() else {
|
let Ok(log_dir) = app.path().app_log_dir() else {
|
||||||
@@ -387,6 +233,20 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
})
|
})
|
||||||
.run(tauri::generate_context!())
|
.build(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.unwrap();
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
app.run(|_app, _event| {});
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
use tauri::RunEvent;
|
||||||
|
app.run(|app, event| match event {
|
||||||
|
RunEvent::Reopen { .. } => {
|
||||||
|
toggle_window_visibility(app);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -17,7 +17,7 @@
|
|||||||
"createUpdaterArtifacts": false
|
"createUpdaterArtifacts": false
|
||||||
},
|
},
|
||||||
"productName": "easytier-gui",
|
"productName": "easytier-gui",
|
||||||
"version": "2.0.1",
|
"version": "2.1.0",
|
||||||
"identifier": "com.kkrainbow.easytier",
|
"identifier": "com.kkrainbow.easytier",
|
||||||
"plugins": {},
|
"plugins": {},
|
||||||
"app": {
|
"app": {
|
||||||
|
Binary file not shown.
@@ -1,3 +1,12 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||||
|
import pkg from '~/../package.json'
|
||||||
|
|
||||||
|
onBeforeMount(async () => {
|
||||||
|
await getCurrentWindow().setTitle(`Easytier GUI: v${pkg.version}`)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</template>
|
</template>
|
||||||
|
4
easytier-gui/src/auto-imports.d.ts
vendored
4
easytier-gui/src/auto-imports.d.ts
vendored
@@ -21,6 +21,7 @@ declare global {
|
|||||||
const definePage: typeof import('unplugin-vue-router/runtime')['definePage']
|
const definePage: typeof import('unplugin-vue-router/runtime')['definePage']
|
||||||
const defineStore: typeof import('pinia')['defineStore']
|
const defineStore: typeof import('pinia')['defineStore']
|
||||||
const effectScope: typeof import('vue')['effectScope']
|
const effectScope: typeof import('vue')['effectScope']
|
||||||
|
const event2human: typeof import('./composables/utils')['event2human']
|
||||||
const generateMenuItem: typeof import('./composables/tray')['generateMenuItem']
|
const generateMenuItem: typeof import('./composables/tray')['generateMenuItem']
|
||||||
const getActivePinia: typeof import('pinia')['getActivePinia']
|
const getActivePinia: typeof import('pinia')['getActivePinia']
|
||||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||||
@@ -44,6 +45,8 @@ declare global {
|
|||||||
const mapWritableState: typeof import('pinia')['mapWritableState']
|
const mapWritableState: typeof import('pinia')['mapWritableState']
|
||||||
const markRaw: typeof import('vue')['markRaw']
|
const markRaw: typeof import('vue')['markRaw']
|
||||||
const nextTick: typeof import('vue')['nextTick']
|
const nextTick: typeof import('vue')['nextTick']
|
||||||
|
const num2ipv4: typeof import('./composables/utils')['num2ipv4']
|
||||||
|
const num2ipv6: typeof import('./composables/utils')['num2ipv6']
|
||||||
const onActivated: typeof import('vue')['onActivated']
|
const onActivated: typeof import('vue')['onActivated']
|
||||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||||
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||||
@@ -81,6 +84,7 @@ declare global {
|
|||||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||||
const shallowRef: typeof import('vue')['shallowRef']
|
const shallowRef: typeof import('vue')['shallowRef']
|
||||||
const storeToRefs: typeof import('pinia')['storeToRefs']
|
const storeToRefs: typeof import('pinia')['storeToRefs']
|
||||||
|
const timeAgoCn: typeof import('./composables/utils')['timeAgoCn']
|
||||||
const toRaw: typeof import('vue')['toRaw']
|
const toRaw: typeof import('vue')['toRaw']
|
||||||
const toRef: typeof import('vue')['toRef']
|
const toRef: typeof import('vue')['toRef']
|
||||||
const toRefs: typeof import('vue')['toRefs']
|
const toRefs: typeof import('vue')['toRefs']
|
||||||
|
@@ -1,308 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import InputGroup from 'primevue/inputgroup'
|
|
||||||
import InputGroupAddon from 'primevue/inputgroupaddon'
|
|
||||||
import { ping } from 'tauri-plugin-vpnservice-api'
|
|
||||||
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 {
|
|
||||||
new URL(query)
|
|
||||||
ret.push(query)
|
|
||||||
}
|
|
||||||
catch (e) {}
|
|
||||||
}
|
|
||||||
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 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()
|
|
||||||
osHostname.value = await ping('ffdklsajflkdsjl') || ''
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex flex-column h-full">
|
|
||||||
<div class="flex flex-column">
|
|
||||||
<div class="w-10/12 self-center mb-3">
|
|
||||||
<Message severity="warn">
|
|
||||||
{{ t('dhcp_experimental_warning') }}
|
|
||||||
</Message>
|
|
||||||
</div>
|
|
||||||
<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>/24</span>
|
|
||||||
</InputGroupAddon>
|
|
||||||
</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) => 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>
|
|
||||||
<Chips
|
|
||||||
id="chips" v-model="curNetwork.proxy_cidrs"
|
|
||||||
:placeholder="t('chips_placeholder', ['10.0.0.0/24'])" separator=" " class="w-full"
|
|
||||||
/>
|
|
||||||
</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,6 +1,9 @@
|
|||||||
|
import type { NetworkTypes } from 'easytier-frontend-lib'
|
||||||
import { addPluginListener } from '@tauri-apps/api/core'
|
import { addPluginListener } from '@tauri-apps/api/core'
|
||||||
|
import { Utils } from 'easytier-frontend-lib'
|
||||||
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 type { Route } from '~/types/network'
|
|
||||||
|
type Route = NetworkTypes.Route
|
||||||
|
|
||||||
const networkStore = useNetworkStore()
|
const networkStore = useNetworkStore()
|
||||||
|
|
||||||
@@ -48,7 +51,7 @@ async function doStartVpn(ipv4Addr: string, cidr: number, routes: string[]) {
|
|||||||
|
|
||||||
console.log('start vpn')
|
console.log('start vpn')
|
||||||
const start_ret = await start_vpn({
|
const start_ret = await start_vpn({
|
||||||
ipv4Addr: `${ipv4Addr}/${cidr}`,
|
ipv4Addr: `${ipv4Addr}`,
|
||||||
routes,
|
routes,
|
||||||
disallowedApplications: ['com.kkrainbow.easytier'],
|
disallowedApplications: ['com.kkrainbow.easytier'],
|
||||||
mtu: 1300,
|
mtu: 1300,
|
||||||
@@ -122,12 +125,17 @@ async function onNetworkInstanceChange() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const virtual_ip = curNetworkInfo?.node_info?.virtual_ipv4
|
const virtual_ip = Utils.ipv4ToString(curNetworkInfo?.my_node_info?.virtual_ipv4.address)
|
||||||
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,6 +1,8 @@
|
|||||||
|
import type { NetworkTypes } from 'easytier-frontend-lib'
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
import type { NetworkConfig, NetworkInstanceRunningInfo } from '~/types/network'
|
type NetworkConfig = NetworkTypes.NetworkConfig
|
||||||
|
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 })
|
||||||
|
@@ -5,12 +5,11 @@ 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 { i18n, loadLanguageAsync } from '~/modules/i18n'
|
import EasyTierFrontendLib, { I18nUtils } from 'easytier-frontend-lib'
|
||||||
|
|
||||||
import { getAutoLaunchStatusAsync, loadAutoLaunchStatusAsync } from './modules/auto_launch'
|
import { getAutoLaunchStatusAsync, loadAutoLaunchStatusAsync } from './modules/auto_launch'
|
||||||
import '~/styles.css'
|
import '~/styles.css'
|
||||||
import 'primeicons/primeicons.css'
|
import 'easytier-frontend-lib/style.css'
|
||||||
import 'primeflex/primeflex.css'
|
|
||||||
|
|
||||||
if (import.meta.env.PROD) {
|
if (import.meta.env.PROD) {
|
||||||
document.addEventListener('keydown', (event) => {
|
document.addEventListener('keydown', (event) => {
|
||||||
@@ -29,7 +28,7 @@ if (import.meta.env.PROD) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
await loadLanguageAsync(localStorage.getItem('lang') || 'en')
|
await I18nUtils.loadLanguageAsync(localStorage.getItem('lang') || 'en')
|
||||||
await loadAutoLaunchStatusAsync(getAutoLaunchStatusAsync())
|
await loadAutoLaunchStatusAsync(getAutoLaunchStatusAsync())
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
@@ -41,18 +40,22 @@ async function main() {
|
|||||||
|
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.use(createPinia())
|
app.use(createPinia())
|
||||||
app.use(i18n, { useScope: 'global' })
|
app.use(EasyTierFrontendLib)
|
||||||
|
// 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: false,
|
cssLayer: {
|
||||||
|
name: 'primevue',
|
||||||
|
order: 'tailwind-base, primevue, tailwind-utilities'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
app.use(ToastService)
|
app.use(ToastService as any)
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,7 +2,16 @@ import { disable, enable, isEnabled } from '@tauri-apps/plugin-autostart'
|
|||||||
|
|
||||||
export async function loadAutoLaunchStatusAsync(target_enable: boolean): Promise<boolean> {
|
export async function loadAutoLaunchStatusAsync(target_enable: boolean): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
target_enable ? await enable() : await disable()
|
if (target_enable) {
|
||||||
|
await enable()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 消除没有配置自启动时进行关闭操作报错
|
||||||
|
try {
|
||||||
|
await disable()
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
localStorage.setItem('auto_launch', JSON.stringify(await isEnabled()))
|
localStorage.setItem('auto_launch', JSON.stringify(await isEnabled()))
|
||||||
return isEnabled()
|
return isEnabled()
|
||||||
}
|
}
|
||||||
|
@@ -8,14 +8,11 @@ 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 Config from '~/components/Config.vue'
|
import { NetworkTypes, Config, Status, Utils, I18nUtils } from 'easytier-frontend-lib'
|
||||||
|
|
||||||
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)
|
||||||
@@ -65,6 +62,27 @@ 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
|
||||||
@@ -82,7 +100,7 @@ networkStore.$subscribe(async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
async function runNetworkCb(cfg: NetworkConfig, cb: () => void) {
|
async function runNetworkCb(cfg: NetworkTypes.NetworkConfig, cb: () => void) {
|
||||||
if (type() === 'android') {
|
if (type() === 'android') {
|
||||||
await prepareVpnService()
|
await prepareVpnService()
|
||||||
networkStore.clearNetworkInstances()
|
networkStore.clearNetworkInstances()
|
||||||
@@ -106,7 +124,7 @@ async function runNetworkCb(cfg: NetworkConfig, cb: () => void) {
|
|||||||
cb()
|
cb()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function stopNetworkCb(cfg: NetworkConfig, cb: () => void) {
|
async function stopNetworkCb(cfg: NetworkTypes.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)
|
||||||
@@ -145,7 +163,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 loadLanguageAsync((locale.value === 'en' ? 'cn' : 'en'))
|
await I18nUtils.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')),
|
||||||
@@ -181,7 +199,7 @@ const setting_menu_items = ref([
|
|||||||
label: () => t('logging_open_dir'),
|
label: () => t('logging_open_dir'),
|
||||||
icon: 'pi pi-folder-open',
|
icon: 'pi pi-folder-open',
|
||||||
command: async () => {
|
command: async () => {
|
||||||
console.log('open log dir', await appLogDir())
|
// console.log('open log dir', await appLogDir())
|
||||||
await open(await appLogDir())
|
await open(await appLogDir())
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -221,7 +239,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 => item.instance_id === id)
|
const cfg = networkStore.networkList.find((item: NetworkTypes.NetworkConfig) => item.instance_id === id)
|
||||||
if (cfg) {
|
if (cfg) {
|
||||||
networkStore.addNetworkInstance(cfg.instance_id)
|
networkStore.addNetworkInstance(cfg.instance_id)
|
||||||
await runNetworkInstance(cfg)
|
await runNetworkInstance(cfg)
|
||||||
@@ -245,7 +263,7 @@ function isRunning(id: string) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div id="root" class="flex flex-column">
|
<div id="root" class="flex flex-col">
|
||||||
<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">
|
||||||
@@ -253,7 +271,7 @@ function isRunning(id: string) {
|
|||||||
</ScrollPanel>
|
</ScrollPanel>
|
||||||
</Panel>
|
</Panel>
|
||||||
<Divider />
|
<Divider />
|
||||||
<div class="flex gap-2 justify-content-end">
|
<div class="flex gap-2 justify-end">
|
||||||
<Button type="button" :label="t('close')" @click="visible = false" />
|
<Button type="button" :label="t('close')" @click="visible = false" />
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@@ -265,65 +283,55 @@ function isRunning(id: string) {
|
|||||||
<div>
|
<div>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<template #start>
|
<template #start>
|
||||||
<div class="flex align-items-center">
|
<div class="flex 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">
|
||||||
<Dropdown
|
<Select v-model="networkStore.curNetwork" :options="networkStore.networkList" :highlight-on-select="false"
|
||||||
v-model="networkStore.curNetwork" :options="networkStore.networkList" :highlight-on-select="false"
|
:placeholder="t('select_network')" class="w-full">
|
||||||
: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-3 flex-column">
|
<div class="mr-4 flex-col">
|
||||||
<span>{{ slotProps.value.network_name }}</span>
|
<span>{{ slotProps.value.network_name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<Tag
|
<Tag class="my-auto leading-3" :severity="isRunning(slotProps.value.instance_id) ? 'success' : 'info'"
|
||||||
class="my-auto leading-3" :severity="isRunning(slotProps.value.instance_id) ? 'success' : 'info'"
|
:value="t(isRunning(slotProps.value.instance_id) ? 'network_running' : 'network_stopped')" />
|
||||||
: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-3">
|
<div class="mr-4">
|
||||||
{{ t('network_name') }}: {{ slotProps.option.network_name }}
|
{{ t('network_name') }}: {{ slotProps.option.network_name }}
|
||||||
</div>
|
</div>
|
||||||
<Tag
|
<Tag class="my-auto leading-3"
|
||||||
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
|
<div v-if="slotProps.option.networking_method !== NetworkTypes.NetworkingMethod.Standalone"
|
||||||
v-if="slotProps.option.networking_method !== NetworkingMethod.Standalone"
|
class="max-w-full overflow-hidden text-ellipsis">
|
||||||
class="max-w-full overflow-hidden text-ellipsis"
|
{{ slotProps.option.networking_method === NetworkTypes.NetworkingMethod.Manual
|
||||||
>
|
|
||||||
{{ 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)">
|
||||||
>
|
{{
|
||||||
{{ networkStore.instances[slotProps.option.instance_id].detail
|
Utils.ipv4InetToString(networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4)
|
||||||
? networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4 : '' }}
|
}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Dropdown>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #end>
|
<template #end>
|
||||||
<Button
|
<Button icon="pi pi-cog" severity="secondary" aria-haspopup="true" :label="t('settings')"
|
||||||
icon="pi pi-cog" severity="secondary" aria-haspopup="true" :label="t('settings')"
|
aria-controls="overlay_setting_menu" @click="toggle_setting_menu" />
|
||||||
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>
|
||||||
@@ -341,20 +349,16 @@ 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
|
<Config :instance-id="networkStore.curNetworkId" :config-invalid="messageBarSeverity !== Severity.None"
|
||||||
:instance-id="networkStore.curNetworkId" :config-invalid="messageBarSeverity !== Severity.None"
|
:cur-network="curNetworkConfig" @run-network="runNetworkCb($event, () => activateCallback('2'))" />
|
||||||
@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-column">
|
<div class="flex flex-col">
|
||||||
<Status :instance-id="networkStore.curNetworkId" />
|
<Status :cur-network-inst="curNetworkInst" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex pt-4 justify-content-center">
|
<div class="flex pt-6 justify-center">
|
||||||
<Button
|
<Button :label="t('stop_network')" severity="danger" icon="pi pi-arrow-left"
|
||||||
:label="t('stop_network')" severity="danger" icon="pi pi-arrow-left"
|
@click="stopNetworkCb(networkStore.curNetwork, () => activateCallback('1'))" />
|
||||||
@click="stopNetworkCb(networkStore.curNetwork, () => activateCallback('1'))"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</StepPanel>
|
</StepPanel>
|
||||||
</StepPanels>
|
</StepPanels>
|
||||||
|
@@ -1,26 +1,25 @@
|
|||||||
import type { NetworkConfig, NetworkInstance, NetworkInstanceRunningInfo } from '~/types/network'
|
import { NetworkTypes } from 'easytier-frontend-lib'
|
||||||
import { DEFAULT_NETWORK_CONFIG } from '~/types/network'
|
|
||||||
|
|
||||||
export const useNetworkStore = defineStore('networkStore', {
|
export const useNetworkStore = defineStore('networkStore', {
|
||||||
state: () => {
|
state: () => {
|
||||||
const networkList = [DEFAULT_NETWORK_CONFIG()]
|
const networkList = [NetworkTypes.DEFAULT_NETWORK_CONFIG()]
|
||||||
return {
|
return {
|
||||||
// for initially empty lists
|
// for initially empty lists
|
||||||
networkList: networkList as NetworkConfig[],
|
networkList: networkList as NetworkTypes.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, NetworkInstance>,
|
instances: {} as Record<string, NetworkTypes.NetworkInstance>,
|
||||||
|
|
||||||
networkInfos: {} as Record<string, NetworkInstanceRunningInfo>,
|
networkInfos: {} as Record<string, NetworkTypes.NetworkInstanceRunningInfo>,
|
||||||
|
|
||||||
autoStartInstIds: [] as string[],
|
autoStartInstIds: [] as string[],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
lastNetwork(): NetworkConfig {
|
lastNetwork(): NetworkTypes.NetworkConfig {
|
||||||
return this.networkList[this.networkList.length - 1]
|
return this.networkList[this.networkList.length - 1]
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -28,7 +27,7 @@ export const useNetworkStore = defineStore('networkStore', {
|
|||||||
return this.curNetwork.instance_id
|
return this.curNetwork.instance_id
|
||||||
},
|
},
|
||||||
|
|
||||||
networkInstances(): Array<NetworkInstance> {
|
networkInstances(): Array<NetworkTypes.NetworkInstance> {
|
||||||
return Object.values(this.instances)
|
return Object.values(this.instances)
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -39,7 +38,7 @@ export const useNetworkStore = defineStore('networkStore', {
|
|||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
addNewNetwork() {
|
addNewNetwork() {
|
||||||
this.networkList.push(DEFAULT_NETWORK_CONFIG())
|
this.networkList.push(NetworkTypes.DEFAULT_NETWORK_CONFIG())
|
||||||
},
|
},
|
||||||
|
|
||||||
delCurNetwork() {
|
delCurNetwork() {
|
||||||
@@ -66,7 +65,7 @@ export const useNetworkStore = defineStore('networkStore', {
|
|||||||
this.instances = {}
|
this.instances = {}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateWithNetworkInfos(networkInfos: Record<string, NetworkInstanceRunningInfo>) {
|
updateWithNetworkInfos(networkInfos: Record<string, NetworkTypes.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)
|
||||||
@@ -79,17 +78,17 @@ export const useNetworkStore = defineStore('networkStore', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
loadFromLocalStorage() {
|
loadFromLocalStorage() {
|
||||||
let networkList: NetworkConfig[]
|
let networkList: NetworkTypes.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 { ...DEFAULT_NETWORK_CONFIG(), ...cfg } as NetworkConfig
|
return { ...NetworkTypes.DEFAULT_NETWORK_CONFIG(), ...cfg } as NetworkTypes.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 = [DEFAULT_NETWORK_CONFIG()]
|
networkList = [NetworkTypes.DEFAULT_NETWORK_CONFIG()]
|
||||||
|
|
||||||
this.networkList = networkList
|
this.networkList = networkList
|
||||||
this.curNetwork = this.networkList[0]
|
this.curNetwork = this.networkList[0]
|
||||||
|
21
easytier-rpc-build/Cargo.toml
Normal file
21
easytier-rpc-build/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
[package]
|
||||||
|
name = "easytier-rpc-build"
|
||||||
|
description = "Protobuf RPC Service Generator for EasyTier"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
homepage = "https://github.com/EasyTier/EasyTier"
|
||||||
|
repository = "https://github.com/EasyTier/EasyTier"
|
||||||
|
authors = ["kkrainbow"]
|
||||||
|
keywords = ["vpn", "p2p", "network", "easytier"]
|
||||||
|
categories = ["network-programming", "command-line-utilities"]
|
||||||
|
rust-version = "1.77.0"
|
||||||
|
license-file = "LICENSE"
|
||||||
|
readme = "README.md"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
heck = "0.5"
|
||||||
|
prost-build = "0.13"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
internal-namespace = []
|
1
easytier-rpc-build/LICENSE
Symbolic link
1
easytier-rpc-build/LICENSE
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../LICENSE
|
3
easytier-rpc-build/README.md
Normal file
3
easytier-rpc-build/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Introduction
|
||||||
|
|
||||||
|
This is a protobuf rpc service stub generator for [EasyTier](https://github.com/EasyTier/EasyTier) project.
|
@@ -3,8 +3,12 @@ extern crate prost_build;
|
|||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
|
#[cfg(feature = "internal-namespace")]
|
||||||
const NAMESPACE: &str = "crate::proto::rpc_types";
|
const NAMESPACE: &str = "crate::proto::rpc_types";
|
||||||
|
|
||||||
|
#[cfg(not(feature = "internal-namespace"))]
|
||||||
|
const NAMESPACE: &str = "easytier::proto::rpc_types";
|
||||||
|
|
||||||
/// The service generator to be used with `prost-build` to generate RPC implementations for
|
/// The service generator to be used with `prost-build` to generate RPC implementations for
|
||||||
/// `prost-simple-rpc`.
|
/// `prost-simple-rpc`.
|
||||||
///
|
///
|
52
easytier-web/Cargo.toml
Normal file
52
easytier-web/Cargo.toml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
[package]
|
||||||
|
name = "easytier-web"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
easytier = { path = "../easytier" }
|
||||||
|
tracing = { version = "0.1", features = ["log"] }
|
||||||
|
anyhow = { version = "1.0" }
|
||||||
|
thiserror = "1.0"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
dashmap = "6.1"
|
||||||
|
url = "2.2"
|
||||||
|
async-trait = "0.1"
|
||||||
|
|
||||||
|
axum = { version = "0.7", features = ["macros"] }
|
||||||
|
axum-login = { version = "0.16" }
|
||||||
|
password-auth = { version = "1.0.0" }
|
||||||
|
axum-messages = "0.7.0"
|
||||||
|
tower-sessions-sqlx-store = { version = "0.14.1", features = ["sqlite"] }
|
||||||
|
tower-sessions = { version = "0.13.0", default-features = false, features = [
|
||||||
|
"signed",
|
||||||
|
] }
|
||||||
|
tower-http = { version = "0.6", features = ["cors", "compression-full"] }
|
||||||
|
sqlx = { version = "0.8", features = ["sqlite"] }
|
||||||
|
sea-orm = { version = "1.1", features = [ "sqlx-sqlite", "runtime-tokio-rustls", "macros" ] }
|
||||||
|
sea-orm-migration = { version = "1.1" }
|
||||||
|
|
||||||
|
|
||||||
|
# for captcha
|
||||||
|
rust-embed = { version = "8.5.0", features = ["debug-embed"] }
|
||||||
|
base64 = "0.22"
|
||||||
|
rand = "0.8"
|
||||||
|
image = {version="0.24", default-features = false, features = ["png"]}
|
||||||
|
rusttype = "0.9.3"
|
||||||
|
imageproc = "0.23.0"
|
||||||
|
|
||||||
|
|
||||||
|
clap = { version = "4.4.8", features = [
|
||||||
|
"string",
|
||||||
|
"unicode",
|
||||||
|
"derive",
|
||||||
|
"wrap_help",
|
||||||
|
] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
uuid = { version = "1.5.0", features = [
|
||||||
|
"v4",
|
||||||
|
"fast-rng",
|
||||||
|
"macro-diagnostics",
|
||||||
|
"serde",
|
||||||
|
] }
|
24
easytier-web/frontend-lib/.gitignore
vendored
Normal file
24
easytier-web/frontend-lib/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
5
easytier-web/frontend-lib/README.md
Normal file
5
easytier-web/frontend-lib/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Vue 3 + TypeScript + Vite
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||||
|
|
||||||
|
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
13
easytier-web/frontend-lib/index.html
Normal file
13
easytier-web/frontend-lib/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + Vue + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
49
easytier-web/frontend-lib/package.json
Normal file
49
easytier-web/frontend-lib/package.json
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"name": "easytier-frontend-lib",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"main": "./dist/easytier-frontend-lib.umd.cjs",
|
||||||
|
"module": "./dist/easytier-frontend-lib.js",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/easytier-frontend-lib.js",
|
||||||
|
"require": "./dist/easytier-frontend-lib.umd.cjs"
|
||||||
|
},
|
||||||
|
"./*.css": "./dist/*.css"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc -b && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@primevue/themes": "^4.2.1",
|
||||||
|
"@vueuse/core": "^11.1.0",
|
||||||
|
"aura": "link:@primevue\\themes\\aura",
|
||||||
|
"axios": "^1.7.7",
|
||||||
|
"floating-vue": "^5.2",
|
||||||
|
"ip-num": "1.5.1",
|
||||||
|
"primeicons": "^7.0.0",
|
||||||
|
"primevue": "^4.2.1",
|
||||||
|
"tailwindcss-primeui": "^0.3.4",
|
||||||
|
"ts-md5": "^1.3.1",
|
||||||
|
"uuid": "^11.0.2",
|
||||||
|
"vue": "^3.5.12",
|
||||||
|
"vue-i18n": "^10.0.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@modyfi/vite-plugin-yaml": "^1.1.0",
|
||||||
|
"@types/node": "^22.8.6",
|
||||||
|
"@vitejs/plugin-vue": "^5.1.4",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"postcss": "^8.4.47",
|
||||||
|
"postcss-import": "^16.1.0",
|
||||||
|
"postcss-nested": "^7.0.2",
|
||||||
|
"tailwindcss": "^3.4.14",
|
||||||
|
"typescript": "~5.6.3",
|
||||||
|
"vite": "^5.4.10",
|
||||||
|
"vite-plugin-dts": "^4.3.0",
|
||||||
|
"vue-tsc": "^2.1.10"
|
||||||
|
}
|
||||||
|
}
|
820
easytier-web/frontend-lib/pnpm-lock.yaml
generated
Normal file
820
easytier-web/frontend-lib/pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,820 @@
|
|||||||
|
lockfileVersion: '9.0'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
importers:
|
||||||
|
|
||||||
|
.:
|
||||||
|
dependencies:
|
||||||
|
vue:
|
||||||
|
specifier: ^3.5.12
|
||||||
|
version: 3.5.12(typescript@5.6.3)
|
||||||
|
devDependencies:
|
||||||
|
'@vitejs/plugin-vue':
|
||||||
|
specifier: ^5.1.4
|
||||||
|
version: 5.1.4(vite@5.4.10)(vue@3.5.12(typescript@5.6.3))
|
||||||
|
typescript:
|
||||||
|
specifier: ~5.6.2
|
||||||
|
version: 5.6.3
|
||||||
|
vite:
|
||||||
|
specifier: ^5.4.10
|
||||||
|
version: 5.4.10
|
||||||
|
vue-tsc:
|
||||||
|
specifier: ^2.1.8
|
||||||
|
version: 2.1.10(typescript@5.6.3)
|
||||||
|
|
||||||
|
packages:
|
||||||
|
|
||||||
|
'@babel/helper-string-parser@7.25.9':
|
||||||
|
resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@babel/helper-validator-identifier@7.25.9':
|
||||||
|
resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@babel/parser@7.26.2':
|
||||||
|
resolution: {integrity: sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==}
|
||||||
|
engines: {node: '>=6.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
'@babel/types@7.26.0':
|
||||||
|
resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@esbuild/aix-ppc64@0.21.5':
|
||||||
|
resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [aix]
|
||||||
|
|
||||||
|
'@esbuild/android-arm64@0.21.5':
|
||||||
|
resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/android-arm@0.21.5':
|
||||||
|
resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/android-x64@0.21.5':
|
||||||
|
resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/darwin-arm64@0.21.5':
|
||||||
|
resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@esbuild/darwin-x64@0.21.5':
|
||||||
|
resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@esbuild/freebsd-arm64@0.21.5':
|
||||||
|
resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@esbuild/freebsd-x64@0.21.5':
|
||||||
|
resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@esbuild/linux-arm64@0.21.5':
|
||||||
|
resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-arm@0.21.5':
|
||||||
|
resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-ia32@0.21.5':
|
||||||
|
resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-loong64@0.21.5':
|
||||||
|
resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [loong64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-mips64el@0.21.5':
|
||||||
|
resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [mips64el]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-ppc64@0.21.5':
|
||||||
|
resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-riscv64@0.21.5':
|
||||||
|
resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-s390x@0.21.5':
|
||||||
|
resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-x64@0.21.5':
|
||||||
|
resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/netbsd-x64@0.21.5':
|
||||||
|
resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [netbsd]
|
||||||
|
|
||||||
|
'@esbuild/openbsd-x64@0.21.5':
|
||||||
|
resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [openbsd]
|
||||||
|
|
||||||
|
'@esbuild/sunos-x64@0.21.5':
|
||||||
|
resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [sunos]
|
||||||
|
|
||||||
|
'@esbuild/win32-arm64@0.21.5':
|
||||||
|
resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@esbuild/win32-ia32@0.21.5':
|
||||||
|
resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@esbuild/win32-x64@0.21.5':
|
||||||
|
resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@jridgewell/sourcemap-codec@1.5.0':
|
||||||
|
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
|
||||||
|
|
||||||
|
'@rollup/rollup-android-arm-eabi@4.24.3':
|
||||||
|
resolution: {integrity: sha512-ufb2CH2KfBWPJok95frEZZ82LtDl0A6QKTa8MoM+cWwDZvVGl5/jNb79pIhRvAalUu+7LD91VYR0nwRD799HkQ==}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@rollup/rollup-android-arm64@4.24.3':
|
||||||
|
resolution: {integrity: sha512-iAHpft/eQk9vkWIV5t22V77d90CRofgR2006UiCjHcHJFVI1E0oBkQIAbz+pLtthFw3hWEmVB4ilxGyBf48i2Q==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@rollup/rollup-darwin-arm64@4.24.3':
|
||||||
|
resolution: {integrity: sha512-QPW2YmkWLlvqmOa2OwrfqLJqkHm7kJCIMq9kOz40Zo9Ipi40kf9ONG5Sz76zszrmIZZ4hgRIkez69YnTHgEz1w==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@rollup/rollup-darwin-x64@4.24.3':
|
||||||
|
resolution: {integrity: sha512-KO0pN5x3+uZm1ZXeIfDqwcvnQ9UEGN8JX5ufhmgH5Lz4ujjZMAnxQygZAVGemFWn+ZZC0FQopruV4lqmGMshow==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@rollup/rollup-freebsd-arm64@4.24.3':
|
||||||
|
resolution: {integrity: sha512-CsC+ZdIiZCZbBI+aRlWpYJMSWvVssPuWqrDy/zi9YfnatKKSLFCe6fjna1grHuo/nVaHG+kiglpRhyBQYRTK4A==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@rollup/rollup-freebsd-x64@4.24.3':
|
||||||
|
resolution: {integrity: sha512-F0nqiLThcfKvRQhZEzMIXOQG4EeX61im61VYL1jo4eBxv4aZRmpin6crnBJQ/nWnCsjH5F6J3W6Stdm0mBNqBg==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-arm-gnueabihf@4.24.3':
|
||||||
|
resolution: {integrity: sha512-KRSFHyE/RdxQ1CSeOIBVIAxStFC/hnBgVcaiCkQaVC+EYDtTe4X7z5tBkFyRoBgUGtB6Xg6t9t2kulnX6wJc6A==}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-arm-musleabihf@4.24.3':
|
||||||
|
resolution: {integrity: sha512-h6Q8MT+e05zP5BxEKz0vi0DhthLdrNEnspdLzkoFqGwnmOzakEHSlXfVyA4HJ322QtFy7biUAVFPvIDEDQa6rw==}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-arm64-gnu@4.24.3':
|
||||||
|
resolution: {integrity: sha512-fKElSyXhXIJ9pqiYRqisfirIo2Z5pTTve5K438URf08fsypXrEkVmShkSfM8GJ1aUyvjakT+fn2W7Czlpd/0FQ==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-arm64-musl@4.24.3':
|
||||||
|
resolution: {integrity: sha512-YlddZSUk8G0px9/+V9PVilVDC6ydMz7WquxozToozSnfFK6wa6ne1ATUjUvjin09jp34p84milxlY5ikueoenw==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-powerpc64le-gnu@4.24.3':
|
||||||
|
resolution: {integrity: sha512-yNaWw+GAO8JjVx3s3cMeG5Esz1cKVzz8PkTJSfYzE5u7A+NvGmbVFEHP+BikTIyYWuz0+DX9kaA3pH9Sqxp69g==}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-riscv64-gnu@4.24.3':
|
||||||
|
resolution: {integrity: sha512-lWKNQfsbpv14ZCtM/HkjCTm4oWTKTfxPmr7iPfp3AHSqyoTz5AgLemYkWLwOBWc+XxBbrU9SCokZP0WlBZM9lA==}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-s390x-gnu@4.24.3':
|
||||||
|
resolution: {integrity: sha512-HoojGXTC2CgCcq0Woc/dn12wQUlkNyfH0I1ABK4Ni9YXyFQa86Fkt2Q0nqgLfbhkyfQ6003i3qQk9pLh/SpAYw==}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-x64-gnu@4.24.3':
|
||||||
|
resolution: {integrity: sha512-mnEOh4iE4USSccBOtcrjF5nj+5/zm6NcNhbSEfR3Ot0pxBwvEn5QVUXcuOwwPkapDtGZ6pT02xLoPaNv06w7KQ==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-x64-musl@4.24.3':
|
||||||
|
resolution: {integrity: sha512-rMTzawBPimBQkG9NKpNHvquIUTQPzrnPxPbCY1Xt+mFkW7pshvyIS5kYgcf74goxXOQk0CP3EoOC1zcEezKXhw==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@rollup/rollup-win32-arm64-msvc@4.24.3':
|
||||||
|
resolution: {integrity: sha512-2lg1CE305xNvnH3SyiKwPVsTVLCg4TmNCF1z7PSHX2uZY2VbUpdkgAllVoISD7JO7zu+YynpWNSKAtOrX3AiuA==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@rollup/rollup-win32-ia32-msvc@4.24.3':
|
||||||
|
resolution: {integrity: sha512-9SjYp1sPyxJsPWuhOCX6F4jUMXGbVVd5obVpoVEi8ClZqo52ViZewA6eFz85y8ezuOA+uJMP5A5zo6Oz4S5rVQ==}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@rollup/rollup-win32-x64-msvc@4.24.3':
|
||||||
|
resolution: {integrity: sha512-HGZgRFFYrMrP3TJlq58nR1xy8zHKId25vhmm5S9jETEfDf6xybPxsavFTJaufe2zgOGYJBskGlj49CwtEuFhWQ==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@types/estree@1.0.6':
|
||||||
|
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
|
||||||
|
|
||||||
|
'@vitejs/plugin-vue@5.1.4':
|
||||||
|
resolution: {integrity: sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A==}
|
||||||
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
vite: ^5.0.0
|
||||||
|
vue: ^3.2.25
|
||||||
|
|
||||||
|
'@volar/language-core@2.4.8':
|
||||||
|
resolution: {integrity: sha512-K/GxMOXGq997bO00cdFhTNuR85xPxj0BEEAy+BaqqayTmy9Tmhfgmq2wpJcVspRhcwfgPoE2/mEJa26emUhG/g==}
|
||||||
|
|
||||||
|
'@volar/source-map@2.4.8':
|
||||||
|
resolution: {integrity: sha512-jeWJBkC/WivdelMwxKkpFL811uH/jJ1kVxa+c7OvG48DXc3VrP7pplSWPP2W1dLMqBxD+awRlg55FQQfiup4cA==}
|
||||||
|
|
||||||
|
'@volar/typescript@2.4.8':
|
||||||
|
resolution: {integrity: sha512-6xkIYJ5xxghVBhVywMoPMidDDAFT1OoQeXwa27HSgJ6AiIKRe61RXLoik+14Z7r0JvnblXVsjsRLmCr42SGzqg==}
|
||||||
|
|
||||||
|
'@vue/compiler-core@3.5.12':
|
||||||
|
resolution: {integrity: sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==}
|
||||||
|
|
||||||
|
'@vue/compiler-dom@3.5.12':
|
||||||
|
resolution: {integrity: sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==}
|
||||||
|
|
||||||
|
'@vue/compiler-sfc@3.5.12':
|
||||||
|
resolution: {integrity: sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw==}
|
||||||
|
|
||||||
|
'@vue/compiler-ssr@3.5.12':
|
||||||
|
resolution: {integrity: sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==}
|
||||||
|
|
||||||
|
'@vue/compiler-vue2@2.7.16':
|
||||||
|
resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==}
|
||||||
|
|
||||||
|
'@vue/language-core@2.1.10':
|
||||||
|
resolution: {integrity: sha512-DAI289d0K3AB5TUG3xDp9OuQ71CnrujQwJrQnfuZDwo6eGNf0UoRlPuaVNO+Zrn65PC3j0oB2i7mNmVPggeGeQ==}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@vue/reactivity@3.5.12':
|
||||||
|
resolution: {integrity: sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==}
|
||||||
|
|
||||||
|
'@vue/runtime-core@3.5.12':
|
||||||
|
resolution: {integrity: sha512-hrMUYV6tpocr3TL3Ad8DqxOdpDe4zuQY4HPY3X/VRh+L2myQO8MFXPAMarIOSGNu0bFAjh1yBkMPXZBqCk62Uw==}
|
||||||
|
|
||||||
|
'@vue/runtime-dom@3.5.12':
|
||||||
|
resolution: {integrity: sha512-q8VFxR9A2MRfBr6/55Q3umyoN7ya836FzRXajPB6/Vvuv0zOPL+qltd9rIMzG/DbRLAIlREmnLsplEF/kotXKA==}
|
||||||
|
|
||||||
|
'@vue/server-renderer@3.5.12':
|
||||||
|
resolution: {integrity: sha512-I3QoeDDeEPZm8yR28JtY+rk880Oqmj43hreIBVTicisFTx/Dl7JpG72g/X7YF8hnQD3IFhkky5i2bPonwrTVPg==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: 3.5.12
|
||||||
|
|
||||||
|
'@vue/shared@3.5.12':
|
||||||
|
resolution: {integrity: sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==}
|
||||||
|
|
||||||
|
alien-signals@0.2.0:
|
||||||
|
resolution: {integrity: sha512-StlonZhBBrsPPwrDjiPAiVTf/rolxffLxVPT60Qv/t88BZ81BvUVzHgGqEFvJ1ii8HXtm1+zU2Icr59tfWEcag==}
|
||||||
|
|
||||||
|
balanced-match@1.0.2:
|
||||||
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
|
|
||||||
|
brace-expansion@2.0.1:
|
||||||
|
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
|
||||||
|
|
||||||
|
csstype@3.1.3:
|
||||||
|
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||||
|
|
||||||
|
de-indent@1.0.2:
|
||||||
|
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
|
||||||
|
|
||||||
|
entities@4.5.0:
|
||||||
|
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||||
|
engines: {node: '>=0.12'}
|
||||||
|
|
||||||
|
esbuild@0.21.5:
|
||||||
|
resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
estree-walker@2.0.2:
|
||||||
|
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||||
|
|
||||||
|
fsevents@2.3.3:
|
||||||
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
he@1.2.0:
|
||||||
|
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
magic-string@0.30.12:
|
||||||
|
resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==}
|
||||||
|
|
||||||
|
minimatch@9.0.5:
|
||||||
|
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
||||||
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
|
|
||||||
|
muggle-string@0.4.1:
|
||||||
|
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
|
||||||
|
|
||||||
|
nanoid@3.3.7:
|
||||||
|
resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
|
||||||
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
path-browserify@1.0.1:
|
||||||
|
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
||||||
|
|
||||||
|
picocolors@1.1.1:
|
||||||
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
|
postcss@8.4.47:
|
||||||
|
resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==}
|
||||||
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
|
rollup@4.24.3:
|
||||||
|
resolution: {integrity: sha512-HBW896xR5HGmoksbi3JBDtmVzWiPAYqp7wip50hjQ67JbDz61nyoMPdqu1DvVW9asYb2M65Z20ZHsyJCMqMyDg==}
|
||||||
|
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
semver@7.6.3:
|
||||||
|
resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
source-map-js@1.2.1:
|
||||||
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
typescript@5.6.3:
|
||||||
|
resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==}
|
||||||
|
engines: {node: '>=14.17'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
vite@5.4.10:
|
||||||
|
resolution: {integrity: sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==}
|
||||||
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@types/node': ^18.0.0 || >=20.0.0
|
||||||
|
less: '*'
|
||||||
|
lightningcss: ^1.21.0
|
||||||
|
sass: '*'
|
||||||
|
sass-embedded: '*'
|
||||||
|
stylus: '*'
|
||||||
|
sugarss: '*'
|
||||||
|
terser: ^5.4.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/node':
|
||||||
|
optional: true
|
||||||
|
less:
|
||||||
|
optional: true
|
||||||
|
lightningcss:
|
||||||
|
optional: true
|
||||||
|
sass:
|
||||||
|
optional: true
|
||||||
|
sass-embedded:
|
||||||
|
optional: true
|
||||||
|
stylus:
|
||||||
|
optional: true
|
||||||
|
sugarss:
|
||||||
|
optional: true
|
||||||
|
terser:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
vscode-uri@3.0.8:
|
||||||
|
resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==}
|
||||||
|
|
||||||
|
vue-tsc@2.1.10:
|
||||||
|
resolution: {integrity: sha512-RBNSfaaRHcN5uqVqJSZh++Gy/YUzryuv9u1aFWhsammDJXNtUiJMNoJ747lZcQ68wUQFx6E73y4FY3D8E7FGMA==}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '>=5.0.0'
|
||||||
|
|
||||||
|
vue@3.5.12:
|
||||||
|
resolution: {integrity: sha512-CLVZtXtn2ItBIi/zHZ0Sg1Xkb7+PU32bJJ8Bmy7ts3jxXTcbfsEfBivFYYWz1Hur+lalqGAh65Coin0r+HRUfg==}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
snapshots:
|
||||||
|
|
||||||
|
'@babel/helper-string-parser@7.25.9': {}
|
||||||
|
|
||||||
|
'@babel/helper-validator-identifier@7.25.9': {}
|
||||||
|
|
||||||
|
'@babel/parser@7.26.2':
|
||||||
|
dependencies:
|
||||||
|
'@babel/types': 7.26.0
|
||||||
|
|
||||||
|
'@babel/types@7.26.0':
|
||||||
|
dependencies:
|
||||||
|
'@babel/helper-string-parser': 7.25.9
|
||||||
|
'@babel/helper-validator-identifier': 7.25.9
|
||||||
|
|
||||||
|
'@esbuild/aix-ppc64@0.21.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-arm64@0.21.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-arm@0.21.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-x64@0.21.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/darwin-arm64@0.21.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/darwin-x64@0.21.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/freebsd-arm64@0.21.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/freebsd-x64@0.21.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-arm64@0.21.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-arm@0.21.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-ia32@0.21.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-loong64@0.21.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-mips64el@0.21.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-ppc64@0.21.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-riscv64@0.21.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-s390x@0.21.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-x64@0.21.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/netbsd-x64@0.21.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/openbsd-x64@0.21.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/sunos-x64@0.21.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-arm64@0.21.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-ia32@0.21.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-x64@0.21.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@jridgewell/sourcemap-codec@1.5.0': {}
|
||||||
|
|
||||||
|
'@rollup/rollup-android-arm-eabi@4.24.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-android-arm64@4.24.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-darwin-arm64@4.24.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-darwin-x64@4.24.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-freebsd-arm64@4.24.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-freebsd-x64@4.24.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-arm-gnueabihf@4.24.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-arm-musleabihf@4.24.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-arm64-gnu@4.24.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-arm64-musl@4.24.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-powerpc64le-gnu@4.24.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-riscv64-gnu@4.24.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-s390x-gnu@4.24.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-x64-gnu@4.24.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-x64-musl@4.24.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-win32-arm64-msvc@4.24.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-win32-ia32-msvc@4.24.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-win32-x64-msvc@4.24.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@types/estree@1.0.6': {}
|
||||||
|
|
||||||
|
'@vitejs/plugin-vue@5.1.4(vite@5.4.10)(vue@3.5.12(typescript@5.6.3))':
|
||||||
|
dependencies:
|
||||||
|
vite: 5.4.10
|
||||||
|
vue: 3.5.12(typescript@5.6.3)
|
||||||
|
|
||||||
|
'@volar/language-core@2.4.8':
|
||||||
|
dependencies:
|
||||||
|
'@volar/source-map': 2.4.8
|
||||||
|
|
||||||
|
'@volar/source-map@2.4.8': {}
|
||||||
|
|
||||||
|
'@volar/typescript@2.4.8':
|
||||||
|
dependencies:
|
||||||
|
'@volar/language-core': 2.4.8
|
||||||
|
path-browserify: 1.0.1
|
||||||
|
vscode-uri: 3.0.8
|
||||||
|
|
||||||
|
'@vue/compiler-core@3.5.12':
|
||||||
|
dependencies:
|
||||||
|
'@babel/parser': 7.26.2
|
||||||
|
'@vue/shared': 3.5.12
|
||||||
|
entities: 4.5.0
|
||||||
|
estree-walker: 2.0.2
|
||||||
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
'@vue/compiler-dom@3.5.12':
|
||||||
|
dependencies:
|
||||||
|
'@vue/compiler-core': 3.5.12
|
||||||
|
'@vue/shared': 3.5.12
|
||||||
|
|
||||||
|
'@vue/compiler-sfc@3.5.12':
|
||||||
|
dependencies:
|
||||||
|
'@babel/parser': 7.26.2
|
||||||
|
'@vue/compiler-core': 3.5.12
|
||||||
|
'@vue/compiler-dom': 3.5.12
|
||||||
|
'@vue/compiler-ssr': 3.5.12
|
||||||
|
'@vue/shared': 3.5.12
|
||||||
|
estree-walker: 2.0.2
|
||||||
|
magic-string: 0.30.12
|
||||||
|
postcss: 8.4.47
|
||||||
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
'@vue/compiler-ssr@3.5.12':
|
||||||
|
dependencies:
|
||||||
|
'@vue/compiler-dom': 3.5.12
|
||||||
|
'@vue/shared': 3.5.12
|
||||||
|
|
||||||
|
'@vue/compiler-vue2@2.7.16':
|
||||||
|
dependencies:
|
||||||
|
de-indent: 1.0.2
|
||||||
|
he: 1.2.0
|
||||||
|
|
||||||
|
'@vue/language-core@2.1.10(typescript@5.6.3)':
|
||||||
|
dependencies:
|
||||||
|
'@volar/language-core': 2.4.8
|
||||||
|
'@vue/compiler-dom': 3.5.12
|
||||||
|
'@vue/compiler-vue2': 2.7.16
|
||||||
|
'@vue/shared': 3.5.12
|
||||||
|
alien-signals: 0.2.0
|
||||||
|
minimatch: 9.0.5
|
||||||
|
muggle-string: 0.4.1
|
||||||
|
path-browserify: 1.0.1
|
||||||
|
optionalDependencies:
|
||||||
|
typescript: 5.6.3
|
||||||
|
|
||||||
|
'@vue/reactivity@3.5.12':
|
||||||
|
dependencies:
|
||||||
|
'@vue/shared': 3.5.12
|
||||||
|
|
||||||
|
'@vue/runtime-core@3.5.12':
|
||||||
|
dependencies:
|
||||||
|
'@vue/reactivity': 3.5.12
|
||||||
|
'@vue/shared': 3.5.12
|
||||||
|
|
||||||
|
'@vue/runtime-dom@3.5.12':
|
||||||
|
dependencies:
|
||||||
|
'@vue/reactivity': 3.5.12
|
||||||
|
'@vue/runtime-core': 3.5.12
|
||||||
|
'@vue/shared': 3.5.12
|
||||||
|
csstype: 3.1.3
|
||||||
|
|
||||||
|
'@vue/server-renderer@3.5.12(vue@3.5.12(typescript@5.6.3))':
|
||||||
|
dependencies:
|
||||||
|
'@vue/compiler-ssr': 3.5.12
|
||||||
|
'@vue/shared': 3.5.12
|
||||||
|
vue: 3.5.12(typescript@5.6.3)
|
||||||
|
|
||||||
|
'@vue/shared@3.5.12': {}
|
||||||
|
|
||||||
|
alien-signals@0.2.0: {}
|
||||||
|
|
||||||
|
balanced-match@1.0.2: {}
|
||||||
|
|
||||||
|
brace-expansion@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
balanced-match: 1.0.2
|
||||||
|
|
||||||
|
csstype@3.1.3: {}
|
||||||
|
|
||||||
|
de-indent@1.0.2: {}
|
||||||
|
|
||||||
|
entities@4.5.0: {}
|
||||||
|
|
||||||
|
esbuild@0.21.5:
|
||||||
|
optionalDependencies:
|
||||||
|
'@esbuild/aix-ppc64': 0.21.5
|
||||||
|
'@esbuild/android-arm': 0.21.5
|
||||||
|
'@esbuild/android-arm64': 0.21.5
|
||||||
|
'@esbuild/android-x64': 0.21.5
|
||||||
|
'@esbuild/darwin-arm64': 0.21.5
|
||||||
|
'@esbuild/darwin-x64': 0.21.5
|
||||||
|
'@esbuild/freebsd-arm64': 0.21.5
|
||||||
|
'@esbuild/freebsd-x64': 0.21.5
|
||||||
|
'@esbuild/linux-arm': 0.21.5
|
||||||
|
'@esbuild/linux-arm64': 0.21.5
|
||||||
|
'@esbuild/linux-ia32': 0.21.5
|
||||||
|
'@esbuild/linux-loong64': 0.21.5
|
||||||
|
'@esbuild/linux-mips64el': 0.21.5
|
||||||
|
'@esbuild/linux-ppc64': 0.21.5
|
||||||
|
'@esbuild/linux-riscv64': 0.21.5
|
||||||
|
'@esbuild/linux-s390x': 0.21.5
|
||||||
|
'@esbuild/linux-x64': 0.21.5
|
||||||
|
'@esbuild/netbsd-x64': 0.21.5
|
||||||
|
'@esbuild/openbsd-x64': 0.21.5
|
||||||
|
'@esbuild/sunos-x64': 0.21.5
|
||||||
|
'@esbuild/win32-arm64': 0.21.5
|
||||||
|
'@esbuild/win32-ia32': 0.21.5
|
||||||
|
'@esbuild/win32-x64': 0.21.5
|
||||||
|
|
||||||
|
estree-walker@2.0.2: {}
|
||||||
|
|
||||||
|
fsevents@2.3.3:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
he@1.2.0: {}
|
||||||
|
|
||||||
|
magic-string@0.30.12:
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/sourcemap-codec': 1.5.0
|
||||||
|
|
||||||
|
minimatch@9.0.5:
|
||||||
|
dependencies:
|
||||||
|
brace-expansion: 2.0.1
|
||||||
|
|
||||||
|
muggle-string@0.4.1: {}
|
||||||
|
|
||||||
|
nanoid@3.3.7: {}
|
||||||
|
|
||||||
|
path-browserify@1.0.1: {}
|
||||||
|
|
||||||
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
|
postcss@8.4.47:
|
||||||
|
dependencies:
|
||||||
|
nanoid: 3.3.7
|
||||||
|
picocolors: 1.1.1
|
||||||
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
rollup@4.24.3:
|
||||||
|
dependencies:
|
||||||
|
'@types/estree': 1.0.6
|
||||||
|
optionalDependencies:
|
||||||
|
'@rollup/rollup-android-arm-eabi': 4.24.3
|
||||||
|
'@rollup/rollup-android-arm64': 4.24.3
|
||||||
|
'@rollup/rollup-darwin-arm64': 4.24.3
|
||||||
|
'@rollup/rollup-darwin-x64': 4.24.3
|
||||||
|
'@rollup/rollup-freebsd-arm64': 4.24.3
|
||||||
|
'@rollup/rollup-freebsd-x64': 4.24.3
|
||||||
|
'@rollup/rollup-linux-arm-gnueabihf': 4.24.3
|
||||||
|
'@rollup/rollup-linux-arm-musleabihf': 4.24.3
|
||||||
|
'@rollup/rollup-linux-arm64-gnu': 4.24.3
|
||||||
|
'@rollup/rollup-linux-arm64-musl': 4.24.3
|
||||||
|
'@rollup/rollup-linux-powerpc64le-gnu': 4.24.3
|
||||||
|
'@rollup/rollup-linux-riscv64-gnu': 4.24.3
|
||||||
|
'@rollup/rollup-linux-s390x-gnu': 4.24.3
|
||||||
|
'@rollup/rollup-linux-x64-gnu': 4.24.3
|
||||||
|
'@rollup/rollup-linux-x64-musl': 4.24.3
|
||||||
|
'@rollup/rollup-win32-arm64-msvc': 4.24.3
|
||||||
|
'@rollup/rollup-win32-ia32-msvc': 4.24.3
|
||||||
|
'@rollup/rollup-win32-x64-msvc': 4.24.3
|
||||||
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
semver@7.6.3: {}
|
||||||
|
|
||||||
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
|
typescript@5.6.3: {}
|
||||||
|
|
||||||
|
vite@5.4.10:
|
||||||
|
dependencies:
|
||||||
|
esbuild: 0.21.5
|
||||||
|
postcss: 8.4.47
|
||||||
|
rollup: 4.24.3
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
vscode-uri@3.0.8: {}
|
||||||
|
|
||||||
|
vue-tsc@2.1.10(typescript@5.6.3):
|
||||||
|
dependencies:
|
||||||
|
'@volar/typescript': 2.4.8
|
||||||
|
'@vue/language-core': 2.1.10(typescript@5.6.3)
|
||||||
|
semver: 7.6.3
|
||||||
|
typescript: 5.6.3
|
||||||
|
|
||||||
|
vue@3.5.12(typescript@5.6.3):
|
||||||
|
dependencies:
|
||||||
|
'@vue/compiler-dom': 3.5.12
|
||||||
|
'@vue/compiler-sfc': 3.5.12
|
||||||
|
'@vue/runtime-dom': 3.5.12
|
||||||
|
'@vue/server-renderer': 3.5.12(vue@3.5.12(typescript@5.6.3))
|
||||||
|
'@vue/shared': 3.5.12
|
||||||
|
optionalDependencies:
|
||||||
|
typescript: 5.6.3
|
7
easytier-web/frontend-lib/postcss.config.js
Normal file
7
easytier-web/frontend-lib/postcss.config.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
"postcss-nested": {},
|
||||||
|
},
|
||||||
|
}
|
1
easytier-web/frontend-lib/public/vite.svg
Normal file
1
easytier-web/frontend-lib/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 |
1
easytier-web/frontend-lib/src/assets/vue.svg
Normal file
1
easytier-web/frontend-lib/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 |
275
easytier-web/frontend-lib/src/components/Config.vue
Normal file
275
easytier-web/frontend-lib/src/components/Config.vue
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import InputGroup from 'primevue/inputgroup'
|
||||||
|
import InputGroupAddon from 'primevue/inputgroupaddon'
|
||||||
|
import { SelectButton, Checkbox, InputText, InputNumber, AutoComplete, Panel, Divider, ToggleButton, Button } from 'primevue'
|
||||||
|
import { DEFAULT_NETWORK_CONFIG, NetworkConfig, NetworkingMethod } from '../types/network'
|
||||||
|
import { defineProps, defineEmits, ref, } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
configInvalid?: boolean
|
||||||
|
hostname?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineEmits(['runNetwork'])
|
||||||
|
|
||||||
|
const curNetwork = defineModel('curNetwork', {
|
||||||
|
type: Object as () => NetworkConfig,
|
||||||
|
default: DEFAULT_NETWORK_CONFIG,
|
||||||
|
})
|
||||||
|
|
||||||
|
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 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 searchListenerSuggestions(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
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="frontend-lib">
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="w-10/12 self-center ">
|
||||||
|
<Panel :header="t('basic_settings')">
|
||||||
|
<div class="flex flex-col gap-y-2">
|
||||||
|
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||||
|
<div class="flex flex-col gap-2 basis-5/12 grow">
|
||||||
|
<div class="flex 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-col 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-col 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-col 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) => 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-col gap-y-2">
|
||||||
|
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||||
|
<div class="flex flex-col gap-2 basis-5/12 grow">
|
||||||
|
<div class="flex 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-col 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', [props.hostname])" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-x-9 flex-wrap w-full">
|
||||||
|
<div class="flex flex-col 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-col 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/12" fluid />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||||
|
<div class="flex flex-col 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="searchListenerSuggestions" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||||
|
<div class="flex flex-col 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-col 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-6 justify-center">
|
||||||
|
<Button :label="t('run_network')" icon="pi pi-arrow-right" icon-pos="right" :disabled="configInvalid"
|
||||||
|
@click="$emit('runNetwork', curNetwork)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
35
easytier-web/frontend-lib/src/components/HumanEvent.vue
Normal file
35
easytier-web/frontend-lib/src/components/HumanEvent.vue
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { EventType } from '../types/network'
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { Fieldset } from 'primevue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
event: {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
}>()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const eventKey = computed(() => {
|
||||||
|
const key = Object.keys(props.event)[0]
|
||||||
|
return Object.keys(EventType).includes(key) ? key : 'Unknown'
|
||||||
|
})
|
||||||
|
|
||||||
|
const eventValue = computed(() => {
|
||||||
|
const value = props.event[eventKey.value]
|
||||||
|
return typeof value === 'object' ? value : value
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Fieldset :legend="t(`event.${eventKey}`)">
|
||||||
|
<template v-if="eventKey !== 'Unknown'">
|
||||||
|
<div v-if="event.DhcpIpv4Changed">
|
||||||
|
{{ `${eventValue[0]} -> ${eventValue[1]}` }}
|
||||||
|
</div>
|
||||||
|
<pre v-else>{{ eventValue }}</pre>
|
||||||
|
</template>
|
||||||
|
<pre v-else>{{ eventValue }}</pre>
|
||||||
|
</Fieldset>
|
||||||
|
</template>
|
@@ -1,40 +1,28 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { IPv4, IPv6 } from 'ip-num/IPNumber'
|
import { useTimeAgo } from '@vueuse/core'
|
||||||
import type { NodeInfo, PeerRoutePair } from '~/types/network'
|
import { IPv4 } from 'ip-num/IPNumber'
|
||||||
|
import { NetworkInstance, type NodeInfo, type PeerRoutePair } from '../types/network'
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||||
|
import { ipv4InetToString, ipv4ToString, ipv6ToString } from '../modules/utils';
|
||||||
|
import { DataTable, Column, Tag, Chip, Button, Dialog, ScrollPanel, Timeline, Divider, Card, } from 'primevue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
instanceId?: string
|
curNetworkInst: NetworkInstance | null,
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { t } = useI18n()
|
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(() => {
|
const peerRouteInfos = computed(() => {
|
||||||
if (curNetworkInst.value) {
|
if (props.curNetworkInst) {
|
||||||
const my_node_info = curNetworkInst.value.detail?.my_node_info
|
const my_node_info = props.curNetworkInst.detail?.my_node_info
|
||||||
return [{
|
return [{
|
||||||
route: {
|
route: {
|
||||||
ipv4_addr: my_node_info?.virtual_ipv4,
|
ipv4_addr: my_node_info?.virtual_ipv4,
|
||||||
hostname: my_node_info?.hostname,
|
hostname: my_node_info?.hostname,
|
||||||
version: my_node_info?.version,
|
version: my_node_info?.version,
|
||||||
},
|
},
|
||||||
}, ...(curNetworkInst.value.detail?.peer_route_pairs || [])]
|
}, ...(props.curNetworkInst.detail?.peer_route_pairs || [])]
|
||||||
}
|
}
|
||||||
|
|
||||||
return []
|
return []
|
||||||
@@ -111,11 +99,18 @@ function version(info: PeerRoutePair) {
|
|||||||
return info.route.version === '' ? 'unknown' : info.route.version
|
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 ? `${IPv4.fromNumber(ip.address.addr)}/${ip.network_length}` : ''
|
||||||
|
}
|
||||||
|
|
||||||
const myNodeInfo = computed(() => {
|
const myNodeInfo = computed(() => {
|
||||||
if (!curNetworkInst.value)
|
if (!props.curNetworkInst)
|
||||||
return {} as NodeInfo
|
return {} as NodeInfo
|
||||||
|
|
||||||
return curNetworkInst.value.detail?.my_node_info
|
return props.curNetworkInst.detail?.my_node_info
|
||||||
})
|
})
|
||||||
|
|
||||||
interface Chip {
|
interface Chip {
|
||||||
@@ -124,16 +119,16 @@ interface Chip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const myNodeInfoChips = computed(() => {
|
const myNodeInfoChips = computed(() => {
|
||||||
if (!curNetworkInst.value)
|
if (!props.curNetworkInst)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
const chips: Array<Chip> = []
|
const chips: Array<Chip> = []
|
||||||
const my_node_info = curNetworkInst.value.detail?.my_node_info
|
const my_node_info = props.curNetworkInst.detail?.my_node_info
|
||||||
if (!my_node_info)
|
if (!my_node_info)
|
||||||
return chips
|
return chips
|
||||||
|
|
||||||
// TUN Device Name
|
// TUN Device Name
|
||||||
const dev_name = curNetworkInst.value.detail?.dev_name
|
const dev_name = props.curNetworkInst.detail?.dev_name
|
||||||
if (dev_name) {
|
if (dev_name) {
|
||||||
chips.push({
|
chips.push({
|
||||||
label: `TUN Device Name: ${dev_name}`,
|
label: `TUN Device Name: ${dev_name}`,
|
||||||
@@ -143,7 +138,7 @@ const myNodeInfoChips = computed(() => {
|
|||||||
|
|
||||||
// virtual ipv4
|
// virtual ipv4
|
||||||
chips.push({
|
chips.push({
|
||||||
label: `Virtual IPv4: ${my_node_info.virtual_ipv4}`,
|
label: `Virtual IPv4: ${ipv4InetToString(my_node_info.virtual_ipv4)}`,
|
||||||
icon: '',
|
icon: '',
|
||||||
} as Chip)
|
} as Chip)
|
||||||
|
|
||||||
@@ -151,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}: ${IPv4.fromNumber(ip.addr)}`,
|
label: `Local IPv4 ${idx}: ${ipv4ToString(ip)}`,
|
||||||
icon: '',
|
icon: '',
|
||||||
} as Chip)
|
} as Chip)
|
||||||
}
|
}
|
||||||
@@ -160,11 +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}: ${IPv6.fromBigInt((BigInt(ip.part1) << BigInt(96))
|
label: `Local IPv6 ${idx}: ${ipv6ToString(ip)}`,
|
||||||
+ (BigInt(ip.part2) << BigInt(64))
|
|
||||||
+ (BigInt(ip.part3) << BigInt(32))
|
|
||||||
+ BigInt(ip.part4),
|
|
||||||
)}`,
|
|
||||||
icon: '',
|
icon: '',
|
||||||
} as Chip)
|
} as Chip)
|
||||||
}
|
}
|
||||||
@@ -181,11 +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: ${IPv6.fromBigInt((BigInt(public_ipv6.part1) << BigInt(96))
|
label: `Public IPv6: ${ipv6ToString(public_ipv6)}`,
|
||||||
+ (BigInt(public_ipv6.part2) << BigInt(64))
|
|
||||||
+ (BigInt(public_ipv6.part3) << BigInt(32))
|
|
||||||
+ BigInt(public_ipv6.part4),
|
|
||||||
)}`,
|
|
||||||
icon: '',
|
icon: '',
|
||||||
} as Chip)
|
} as Chip)
|
||||||
}
|
}
|
||||||
@@ -194,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}`,
|
label: `Listener ${idx}: ${listener.url}`,
|
||||||
icon: '',
|
icon: '',
|
||||||
} as Chip)
|
} as Chip)
|
||||||
}
|
}
|
||||||
@@ -210,6 +197,8 @@ const myNodeInfoChips = computed(() => {
|
|||||||
PortRestricted = 5,
|
PortRestricted = 5,
|
||||||
Symmetric = 6,
|
Symmetric = 6,
|
||||||
SymUdpFirewall = 7,
|
SymUdpFirewall = 7,
|
||||||
|
SymmetricEasyInc = 8,
|
||||||
|
SymmetricEasyDec = 9,
|
||||||
};
|
};
|
||||||
const udpNatType: NatType = my_node_info.stun_info?.udp_nat_type
|
const udpNatType: NatType = my_node_info.stun_info?.udp_nat_type
|
||||||
if (udpNatType !== undefined) {
|
if (udpNatType !== undefined) {
|
||||||
@@ -222,6 +211,8 @@ const myNodeInfoChips = computed(() => {
|
|||||||
[NatType.PortRestricted]: 'Port Restricted',
|
[NatType.PortRestricted]: 'Port Restricted',
|
||||||
[NatType.Symmetric]: 'Symmetric',
|
[NatType.Symmetric]: 'Symmetric',
|
||||||
[NatType.SymUdpFirewall]: 'Symmetric UDP Firewall',
|
[NatType.SymUdpFirewall]: 'Symmetric UDP Firewall',
|
||||||
|
[NatType.SymmetricEasyInc]: 'Symmetric Easy Inc',
|
||||||
|
[NatType.SymmetricEasyDec]: 'Symmetric Easy Dec',
|
||||||
}
|
}
|
||||||
|
|
||||||
chips.push({
|
chips.push({
|
||||||
@@ -300,28 +291,31 @@ function showVpnPortalConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showEventLogs() {
|
function showEventLogs() {
|
||||||
const detail = curNetworkInst.value?.detail
|
const detail = props.curNetworkInst?.detail
|
||||||
if (!detail)
|
if (!detail)
|
||||||
return
|
return
|
||||||
|
|
||||||
dialogContent.value = detail.events
|
dialogContent.value = detail.events.map((event: string) => JSON.parse(event))
|
||||||
dialogHeader.value = 'event_log'
|
dialogHeader.value = 'event_log'
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="frontend-lib">
|
||||||
<Dialog v-model:visible="dialogVisible" modal :header="t(dialogHeader)" :style="{ width: '70%' }">
|
<Dialog v-model:visible="dialogVisible" modal :header="t(dialogHeader)" class="w-2/3 h-auto">
|
||||||
<Panel>
|
<ScrollPanel v-if="dialogHeader === 'vpn_portal_config'">
|
||||||
<ScrollPanel style="width: 100%; height: 400px">
|
<pre>{{ dialogContent }}</pre>
|
||||||
<pre>{{ dialogContent }}</pre>
|
</ScrollPanel>
|
||||||
</ScrollPanel>
|
<Timeline v-else :value="dialogContent">
|
||||||
</Panel>
|
<template #opposite="slotProps">
|
||||||
<Divider />
|
<small class="text-surface-500 dark:text-surface-400">{{ useTimeAgo(Date.parse(slotProps.item.time))
|
||||||
<div class="flex justify-content-end gap-2">
|
}}</small>
|
||||||
<Button type="button" :label="t('close')" @click="dialogVisible = false" />
|
</template>
|
||||||
</div>
|
<template #content="slotProps">
|
||||||
|
<HumanEvent :event="slotProps.item.event" />
|
||||||
|
</template>
|
||||||
|
</Timeline>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Card v-if="curNetworkInst?.error_msg">
|
<Card v-if="curNetworkInst?.error_msg">
|
||||||
@@ -329,7 +323,7 @@ function showEventLogs() {
|
|||||||
Run Network Error
|
Run Network Error
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="flex flex-column gap-y-5">
|
<div class="flex flex-col gap-y-5">
|
||||||
<div class="text-red-500">
|
<div class="text-red-500">
|
||||||
{{ curNetworkInst.error_msg }}
|
{{ curNetworkInst.error_msg }}
|
||||||
</div>
|
</div>
|
||||||
@@ -343,12 +337,9 @@ function showEventLogs() {
|
|||||||
{{ t('my_node_info') }}
|
{{ t('my_node_info') }}
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="flex w-full flex-column gap-y-5">
|
<div class="flex w-full flex-col gap-y-5">
|
||||||
<div class="m-0 flex flex-row justify-center gap-x-5">
|
<div class="m-0 flex flex-row justify-center gap-x-5">
|
||||||
<div
|
<div class="rounded-full w-32 h-32 flex flex-col items-center pt-6" style="border: 1px solid green">
|
||||||
class="rounded-full w-32 h-32 flex flex-column align-items-center pt-4"
|
|
||||||
style="border: 1px solid green"
|
|
||||||
>
|
|
||||||
<div class="font-bold">
|
<div class="font-bold">
|
||||||
{{ t('peer_count') }}
|
{{ t('peer_count') }}
|
||||||
</div>
|
</div>
|
||||||
@@ -357,10 +348,7 @@ function showEventLogs() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="rounded-full w-32 h-32 flex flex-col items-center pt-6" style="border: 1px solid purple">
|
||||||
class="rounded-full w-32 h-32 flex flex-column align-items-center pt-4"
|
|
||||||
style="border: 1px solid purple"
|
|
||||||
>
|
|
||||||
<div class="font-bold">
|
<div class="font-bold">
|
||||||
{{ t('upload') }}
|
{{ t('upload') }}
|
||||||
</div>
|
</div>
|
||||||
@@ -369,10 +357,7 @@ function showEventLogs() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="rounded-full w-32 h-32 flex flex-col items-center pt-6" style="border: 1px solid fuchsia">
|
||||||
class="rounded-full w-32 h-32 flex flex-column align-items-center pt-4"
|
|
||||||
style="border: 1px solid fuchsia"
|
|
||||||
>
|
|
||||||
<div class="font-bold">
|
<div class="font-bold">
|
||||||
{{ t('download') }}
|
{{ t('download') }}
|
||||||
</div>
|
</div>
|
||||||
@@ -382,11 +367,9 @@ function showEventLogs() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-row align-items-center flex-wrap w-full max-h-40 overflow-scroll">
|
<div class="flex flex-row items-center flex-wrap w-full max-h-40 overflow-scroll">
|
||||||
<Chip
|
<Chip v-for="(chip, i) in myNodeInfoChips" :key="i" :label="chip.label" :icon="chip.icon"
|
||||||
v-for="(chip, i) in myNodeInfoChips" :key="i" :label="chip.label" :icon="chip.icon"
|
class="mr-2 mt-2 text-sm" />
|
||||||
class="mr-2 mt-2 text-sm"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="myNodeInfo" class="m-0 flex flex-row justify-center gap-x-5 text-sm">
|
<div v-if="myNodeInfo" class="m-0 flex flex-row justify-center gap-x-5 text-sm">
|
||||||
@@ -404,18 +387,44 @@ function showEventLogs() {
|
|||||||
{{ t('peer_info') }}
|
{{ t('peer_info') }}
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<DataTable :value="peerRouteInfos" column-resize-mode="fit" table-style="width: 100%">
|
<DataTable :value="peerRouteInfos" column-resize-mode="fit" table-class="w-full">
|
||||||
<Column field="route.ipv4_addr" style="width: 100px;" :header="t('virtual_ipv4')" />
|
<Column :field="ipFormat" :header="t('virtual_ipv4')" />
|
||||||
<Column field="route.hostname" style="max-width: 250px;" :header="t('hostname')" />
|
<Column :header="t('hostname')">
|
||||||
<Column :field="routeCost" style="width: 100px;" :header="t('route_cost')" />
|
<template #body="slotProps">
|
||||||
<Column :field="latencyMs" style="width: 80px;" :header="t('latency')" />
|
<div v-if="!slotProps.data.route.cost || !slotProps.data.route.feature_flag.is_public_server"
|
||||||
<Column :field="txBytes" style="width: 80px;" :header="t('upload_bytes')" />
|
v-tooltip="slotProps.data.route.hostname">
|
||||||
<Column :field="rxBytes" style="width: 80px;" :header="t('download_bytes')" />
|
{{
|
||||||
<Column :field="lossRate" style="width: 100px;" :header="t('loss_rate')" />
|
slotProps.data.route.hostname }}
|
||||||
<Column :field="version" style="width: 100px;" :header="t('status.version')" />
|
</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.feature_flag.avoid_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>
|
</DataTable>
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="postcss" scoped>
|
||||||
|
.p-timeline :deep(.p-timeline-event-opposite) {
|
||||||
|
@apply flex-none;
|
||||||
|
}
|
||||||
|
</style>
|
2
easytier-web/frontend-lib/src/components/index.ts
Normal file
2
easytier-web/frontend-lib/src/components/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as Config } from './Config.vue';
|
||||||
|
export { default as Status } from './Status.vue';
|
50
easytier-web/frontend-lib/src/easytier-frontend-lib.ts
Normal file
50
easytier-web/frontend-lib/src/easytier-frontend-lib.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import './style.css'
|
||||||
|
|
||||||
|
import type { App } from 'vue';
|
||||||
|
import { Config, Status } from "./components";
|
||||||
|
import Aura from '@primevue/themes/aura'
|
||||||
|
import PrimeVue from 'primevue/config'
|
||||||
|
|
||||||
|
import I18nUtils from './modules/i18n'
|
||||||
|
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 {
|
||||||
|
install: (app: App): void => {
|
||||||
|
app.use(I18nUtils.i18n, { useScope: 'global' })
|
||||||
|
app.use(PrimeVue, {
|
||||||
|
theme: {
|
||||||
|
preset: Aura,
|
||||||
|
options: {
|
||||||
|
prefix: 'p',
|
||||||
|
darkModeSelector: 'system',
|
||||||
|
cssLayer: {
|
||||||
|
name: 'primevue',
|
||||||
|
order: 'tailwind-base, primevue, tailwind-utilities'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
zIndex: {
|
||||||
|
modal: 1100, //dialog, drawer
|
||||||
|
overlay: 1200, //select, popover
|
||||||
|
menu: 1300, //overlay menus
|
||||||
|
tooltip: 1400 //tooltip
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.component('Config', Config);
|
||||||
|
app.component('Status', Status);
|
||||||
|
app.component('HumanEvent', HumanEvent);
|
||||||
|
app.directive('tooltip', vTooltip as any);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Config, Status, I18nUtils, NetworkTypes, Api, Utils };
|
115
easytier-web/frontend-lib/src/locales/cn.yaml
Normal file
115
easytier-web/frontend-lib/src/locales/cn.yaml
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
network: 网络
|
||||||
|
networking_method: 网络方式
|
||||||
|
public_server: 公共服务器
|
||||||
|
manual: 手动
|
||||||
|
standalone: 独立
|
||||||
|
virtual_ipv4: 虚拟IPv4地址
|
||||||
|
virtual_ipv4_dhcp: DHCP
|
||||||
|
network_name: 网络名称
|
||||||
|
network_secret: 网络密码
|
||||||
|
public_server_url: 公共服务器地址
|
||||||
|
peer_urls: 对等节点地址
|
||||||
|
proxy_cidrs: 子网代理CIDR
|
||||||
|
enable_vpn_portal: 启用VPN门户
|
||||||
|
vpn_portal_listen_port: 监听端口
|
||||||
|
vpn_portal_client_network: 客户端子网
|
||||||
|
dev_name: TUN接口名称
|
||||||
|
advanced_settings: 高级设置
|
||||||
|
basic_settings: 基础设置
|
||||||
|
listener_urls: 监听地址
|
||||||
|
rpc_port: RPC端口
|
||||||
|
config_network: 配置网络
|
||||||
|
running: 运行中
|
||||||
|
error_msg: 错误信息
|
||||||
|
detail: 详情
|
||||||
|
add_new_network: 添加新网络
|
||||||
|
del_cur_network: 删除当前网络
|
||||||
|
select_network: 选择网络
|
||||||
|
network_instances: 网络实例
|
||||||
|
instance_id: 实例ID
|
||||||
|
network_infos: 网络信息
|
||||||
|
parse_network_config: 解析网络配置
|
||||||
|
retain_network_instance: 保留网络实例
|
||||||
|
collect_network_infos: 收集网络信息
|
||||||
|
settings: 设置
|
||||||
|
exchange_language: Switch to English
|
||||||
|
logging: 日志
|
||||||
|
logging_level_info: 信息
|
||||||
|
logging_level_debug: 调试
|
||||||
|
logging_level_warn: 警告
|
||||||
|
logging_level_trace: 跟踪
|
||||||
|
logging_level_off: 关闭
|
||||||
|
logging_open_dir: 打开日志目录
|
||||||
|
logging_copy_dir: 复制日志路径
|
||||||
|
disable_auto_launch: 关闭开机自启
|
||||||
|
enable_auto_launch: 开启开机自启
|
||||||
|
exit: 退出
|
||||||
|
chips_placeholder: 例如: {0}, 按回车添加
|
||||||
|
hostname_placeholder: '留空默认为主机名: {0}'
|
||||||
|
dev_name_placeholder: 注意:当多个网络同时使用相同的TUN接口名称时,将会在设置TUN的IP时产生冲突,留空以自动生成随机名称
|
||||||
|
off_text: 点击关闭
|
||||||
|
on_text: 点击开启
|
||||||
|
show_config: 显示配置
|
||||||
|
close: 关闭
|
||||||
|
|
||||||
|
use_latency_first: 延迟优先模式
|
||||||
|
my_node_info: 当前节点信息
|
||||||
|
peer_count: 已连接
|
||||||
|
upload: 上传
|
||||||
|
download: 下载
|
||||||
|
show_vpn_portal_config: 显示VPN门户配置
|
||||||
|
vpn_portal_config: VPN门户配置
|
||||||
|
show_event_log: 显示事件日志
|
||||||
|
event_log: 事件日志
|
||||||
|
peer_info: 节点信息
|
||||||
|
hostname: 主机名
|
||||||
|
route_cost: 路由
|
||||||
|
latency: 延迟
|
||||||
|
upload_bytes: 上传
|
||||||
|
download_bytes: 下载
|
||||||
|
loss_rate: 丢包率
|
||||||
|
|
||||||
|
status:
|
||||||
|
version: 内核版本
|
||||||
|
local: 本机
|
||||||
|
server: 服务器
|
||||||
|
relay: 中继
|
||||||
|
|
||||||
|
run_network: 运行网络
|
||||||
|
stop_network: 停止网络
|
||||||
|
network_running: 运行中
|
||||||
|
network_stopped: 已停止
|
||||||
|
dhcp_experimental_warning: 实验性警告!使用DHCP时如果组网环境中发生IP冲突,将自动更改IP。
|
||||||
|
|
||||||
|
tray:
|
||||||
|
show: 显示 / 隐藏
|
||||||
|
exit: 退出
|
||||||
|
|
||||||
|
about:
|
||||||
|
title: 关于
|
||||||
|
version: 版本
|
||||||
|
author: 作者
|
||||||
|
homepage: 主页
|
||||||
|
license: 许可证
|
||||||
|
description: 一个简单、安全、去中心化的内网穿透 VPN 组网方案,使用 Rust 语言和 Tokio 框架实现。
|
||||||
|
check_update: 检查更新
|
||||||
|
|
||||||
|
event:
|
||||||
|
Unknown: 未知
|
||||||
|
TunDeviceReady: Tun设备就绪
|
||||||
|
TunDeviceError: Tun设备错误
|
||||||
|
PeerAdded: 对端添加
|
||||||
|
PeerRemoved: 对端移除
|
||||||
|
PeerConnAdded: 对端连接添加
|
||||||
|
PeerConnRemoved: 对端连接移除
|
||||||
|
ListenerAdded: 监听器添加
|
||||||
|
ListenerAddFailed: 监听器添加失败
|
||||||
|
ListenerAcceptFailed: 监听器接受连接失败
|
||||||
|
ConnectionAccepted: 连接已接受
|
||||||
|
ConnectionError: 连接错误
|
||||||
|
Connecting: 正在连接
|
||||||
|
ConnectError: 连接错误
|
||||||
|
VpnPortalClientConnected: VPN门户客户端已连接
|
||||||
|
VpnPortalClientDisconnected: VPN门户客户端已断开连接
|
||||||
|
DhcpIpv4Changed: DHCP IPv4地址更改
|
||||||
|
DhcpIpv4Conflicted: DHCP IPv4地址冲突
|
114
easytier-web/frontend-lib/src/locales/en.yaml
Normal file
114
easytier-web/frontend-lib/src/locales/en.yaml
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
network: Network
|
||||||
|
networking_method: Networking Method
|
||||||
|
public_server: Public Server
|
||||||
|
manual: Manual
|
||||||
|
standalone: Standalone
|
||||||
|
virtual_ipv4: Virtual IPv4
|
||||||
|
virtual_ipv4_dhcp: DHCP
|
||||||
|
network_name: Network Name
|
||||||
|
network_secret: Network Secret
|
||||||
|
public_server_url: Public Server URL
|
||||||
|
peer_urls: Peer URLs
|
||||||
|
proxy_cidrs: Subnet Proxy CIDRs
|
||||||
|
enable_vpn_portal: Enable VPN Portal
|
||||||
|
vpn_portal_listen_port: VPN Portal Listen Port
|
||||||
|
vpn_portal_client_network: Client Sub Network
|
||||||
|
dev_name: TUN interface name
|
||||||
|
advanced_settings: Advanced Settings
|
||||||
|
basic_settings: Basic Settings
|
||||||
|
listener_urls: Listener URLs
|
||||||
|
rpc_port: RPC Port
|
||||||
|
config_network: Config Network
|
||||||
|
running: Running
|
||||||
|
error_msg: Error Message
|
||||||
|
detail: Detail
|
||||||
|
add_new_network: New Network
|
||||||
|
del_cur_network: Delete Current Network
|
||||||
|
select_network: Select Network
|
||||||
|
network_instances: Network Instances
|
||||||
|
instance_id: Instance ID
|
||||||
|
network_infos: Network Infos
|
||||||
|
parse_network_config: Parse Network Config
|
||||||
|
retain_network_instance: Retain Network Instance
|
||||||
|
collect_network_infos: Collect Network Infos
|
||||||
|
settings: Settings
|
||||||
|
exchange_language: 切换中文
|
||||||
|
logging: Logging
|
||||||
|
logging_level_info: Info
|
||||||
|
logging_level_debug: Debug
|
||||||
|
logging_level_warn: Warn
|
||||||
|
logging_level_trace: Trace
|
||||||
|
logging_level_off: Off
|
||||||
|
logging_open_dir: Open Log Directory
|
||||||
|
logging_copy_dir: Copy Log Path
|
||||||
|
disable_auto_launch: Disable Launch on Reboot
|
||||||
|
enable_auto_launch: Enable Launch on Reboot
|
||||||
|
exit: Exit
|
||||||
|
use_latency_first: Latency First Mode
|
||||||
|
chips_placeholder: 'e.g: {0}, press Enter to add'
|
||||||
|
hostname_placeholder: 'Leave blank and default to host name: {0}'
|
||||||
|
dev_name_placeholder: 'Note: When multiple networks use the same TUN interface name at the same time, there will be a conflict when setting the TUN''s IP. Leave blank to automatically generate a random name.'
|
||||||
|
off_text: Press to disable
|
||||||
|
on_text: Press to enable
|
||||||
|
show_config: Show Config
|
||||||
|
close: Close
|
||||||
|
my_node_info: My Node Info
|
||||||
|
peer_count: Connected
|
||||||
|
upload: Upload
|
||||||
|
download: Download
|
||||||
|
show_vpn_portal_config: Show VPN Portal Config
|
||||||
|
vpn_portal_config: VPN Portal Config
|
||||||
|
show_event_log: Show Event Log
|
||||||
|
event_log: Event Log
|
||||||
|
peer_info: Peer Info
|
||||||
|
route_cost: Route Cost
|
||||||
|
hostname: Hostname
|
||||||
|
latency: Latency
|
||||||
|
upload_bytes: Upload
|
||||||
|
download_bytes: Download
|
||||||
|
loss_rate: Loss Rate
|
||||||
|
|
||||||
|
status:
|
||||||
|
version: Version
|
||||||
|
local: Local
|
||||||
|
server: Server
|
||||||
|
relay: Relay
|
||||||
|
|
||||||
|
run_network: Run Network
|
||||||
|
stop_network: Stop Network
|
||||||
|
network_running: running
|
||||||
|
network_stopped: stopped
|
||||||
|
dhcp_experimental_warning: Experimental warning! if there is an IP conflict in the network when using DHCP, the IP will be automatically changed.
|
||||||
|
|
||||||
|
tray:
|
||||||
|
show: Show / Hide
|
||||||
|
exit: Exit
|
||||||
|
|
||||||
|
about:
|
||||||
|
title: About
|
||||||
|
version: Version
|
||||||
|
author: Author
|
||||||
|
homepage: Homepage
|
||||||
|
license: License
|
||||||
|
description: 'EasyTier is a simple, safe and decentralized VPN networking solution implemented with the Rust language and Tokio framework.'
|
||||||
|
check_update: Check Update
|
||||||
|
|
||||||
|
event:
|
||||||
|
Unknown: Unknown
|
||||||
|
TunDeviceReady: TunDeviceReady
|
||||||
|
TunDeviceError: TunDeviceError
|
||||||
|
PeerAdded: PeerAdded
|
||||||
|
PeerRemoved: PeerRemoved
|
||||||
|
PeerConnAdded: PeerConnAdded
|
||||||
|
PeerConnRemoved: PeerConnRemoved
|
||||||
|
ListenerAdded: ListenerAdded
|
||||||
|
ListenerAddFailed: ListenerAddFailed
|
||||||
|
ListenerAcceptFailed: ListenerAcceptFailed
|
||||||
|
ConnectionAccepted: ConnectionAccepted
|
||||||
|
ConnectionError: ConnectionError
|
||||||
|
Connecting: Connecting
|
||||||
|
ConnectError: ConnectError
|
||||||
|
VpnPortalClientConnected: VpnPortalClientConnected
|
||||||
|
VpnPortalClientDisconnected: VpnPortalClientDisconnected
|
||||||
|
DhcpIpv4Changed: DhcpIpv4Changed
|
||||||
|
DhcpIpv4Conflicted: DhcpIpv4Conflicted
|
181
easytier-web/frontend-lib/src/modules/api.ts
Normal file
181
easytier-web/frontend-lib/src/modules/api.ts
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||||
|
import { Md5 } from 'ts-md5'
|
||||||
|
|
||||||
|
export interface ValidateConfigResponse {
|
||||||
|
toml_config: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义接口返回的数据结构
|
||||||
|
export interface LoginResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义请求体数据结构
|
||||||
|
export interface Credential {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterData {
|
||||||
|
credentials: Credential;
|
||||||
|
captcha: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Summary {
|
||||||
|
device_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ApiClient {
|
||||||
|
private client: AxiosInstance;
|
||||||
|
private authFailedCb: Function | undefined;
|
||||||
|
|
||||||
|
constructor(baseUrl: string, authFailedCb: Function | undefined = undefined) {
|
||||||
|
this.client = axios.create({
|
||||||
|
baseURL: baseUrl + '/api/v1',
|
||||||
|
withCredentials: true, // 如果需要支持跨域携带cookie
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.authFailedCb = authFailedCb;
|
||||||
|
|
||||||
|
// 添加请求拦截器
|
||||||
|
this.client.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
||||||
|
return config;
|
||||||
|
}, (error: any) => {
|
||||||
|
return Promise.reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加响应拦截器
|
||||||
|
this.client.interceptors.response.use((response: AxiosResponse) => {
|
||||||
|
console.debug('Axios Response:', response);
|
||||||
|
return response.data; // 假设服务器返回的数据都在data属性中
|
||||||
|
}, (error: any) => {
|
||||||
|
if (error.response) {
|
||||||
|
let response: AxiosResponse = error.response;
|
||||||
|
if (response.status == 401 && this.authFailedCb) {
|
||||||
|
console.error('Unauthorized:', response.data);
|
||||||
|
this.authFailedCb();
|
||||||
|
} else {
|
||||||
|
// 请求已发出,但是服务器响应的状态码不在2xx范围
|
||||||
|
console.error('Response Error:', error.response.data);
|
||||||
|
}
|
||||||
|
} else if (error.request) {
|
||||||
|
// 请求已发出,但是没有收到响应
|
||||||
|
console.error('Request Error:', error.request);
|
||||||
|
} else {
|
||||||
|
// 发生了一些问题导致请求未发出
|
||||||
|
console.error('Error:', error.message);
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册
|
||||||
|
public async register(data: RegisterData): Promise<RegisterResponse> {
|
||||||
|
try {
|
||||||
|
data.credentials.password = Md5.hashStr(data.credentials.password);
|
||||||
|
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> {
|
||||||
|
try {
|
||||||
|
data.password = Md5.hashStr(data.password);
|
||||||
|
const response = await this.client.post<any>('/auth/login', data);
|
||||||
|
console.log("login response:", response);
|
||||||
|
return { success: true, message: 'Login success', };
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
return { success: false, message: 'Invalid username or password', };
|
||||||
|
} else {
|
||||||
|
return { success: false, message: 'Unknown error, status code: ' + error.response?.status, };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { success: false, message: 'Unknown error, error: ' + error, };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async logout() {
|
||||||
|
await this.client.get('/auth/logout');
|
||||||
|
if (this.authFailedCb) {
|
||||||
|
this.authFailedCb();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async change_password(new_password: string) {
|
||||||
|
await this.client.put('/auth/password', { new_password: Md5.hashStr(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() {
|
||||||
|
const response = await this.client.get('/sessions');
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async list_machines(): Promise<Array<any>> {
|
||||||
|
const response = await this.client.get<any, Record<string, Array<any>>>('/machines');
|
||||||
|
return response.machines;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async get_network_info(machine_id: string, inst_id: string): Promise<any> {
|
||||||
|
const response = await this.client.get<any, Record<string, any>>('/machines/' + machine_id + '/networks/info/' + inst_id);
|
||||||
|
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> {
|
||||||
|
const response = await this.client.post<any, ValidateConfigResponse>(`/machines/${machine_id}/validate-config`, {
|
||||||
|
config: config,
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async run_network(machine_id: string, config: any): Promise<undefined> {
|
||||||
|
await this.client.post<string>(`/machines/${machine_id}/networks`, {
|
||||||
|
config: config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async delete_network(machine_id: string, inst_id: string): Promise<undefined> {
|
||||||
|
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() {
|
||||||
|
return this.client.defaults.baseURL + '/auth/captcha';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApiClient;
|
@@ -1,6 +1,9 @@
|
|||||||
import { createI18n } from 'vue-i18n'
|
import { createI18n } from 'vue-i18n'
|
||||||
import type { Locale } from 'vue-i18n'
|
import type { Locale } from 'vue-i18n'
|
||||||
|
|
||||||
|
import EnLocale from '../locales/en.yaml'
|
||||||
|
import CnLocale from '../locales/cn.yaml'
|
||||||
|
|
||||||
// Import i18n resources
|
// Import i18n resources
|
||||||
// https://vitejs.dev/guide/features.html#glob-import
|
// https://vitejs.dev/guide/features.html#glob-import
|
||||||
export const i18n = createI18n({
|
export const i18n = createI18n({
|
||||||
@@ -10,10 +13,10 @@ export const i18n = createI18n({
|
|||||||
messages: {},
|
messages: {},
|
||||||
})
|
})
|
||||||
|
|
||||||
const localesMap = Object.fromEntries(
|
const localesMap = {
|
||||||
Object.entries(import.meta.glob('../../locales/*.yml'))
|
"en": EnLocale,
|
||||||
.map(([path, loadLocale]) => [path.match(/([\w-]*)\.yml$/)?.[1], loadLocale]),
|
"cn": CnLocale,
|
||||||
) as Record<Locale, () => Promise<{ default: Record<string, string> }>>
|
} as Record<string, any>
|
||||||
|
|
||||||
export const availableLocales = Object.keys(localesMap)
|
export const availableLocales = Object.keys(localesMap)
|
||||||
|
|
||||||
@@ -38,13 +41,19 @@ export async function loadLanguageAsync(lang: string): Promise<Locale> {
|
|||||||
let messages
|
let messages
|
||||||
|
|
||||||
try {
|
try {
|
||||||
messages = await localesMap[lang]()
|
messages = localesMap[lang]
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
messages = await localesMap.en()
|
messages = localesMap.en
|
||||||
}
|
}
|
||||||
|
|
||||||
i18n.global.setLocaleMessage(lang, messages.default)
|
i18n.global.setLocaleMessage(lang, messages)
|
||||||
loadedLanguages.push(lang)
|
loadedLanguages.push(lang)
|
||||||
return setI18nLanguage(lang)
|
return setI18nLanguage(lang)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
i18n,
|
||||||
|
localesMap,
|
||||||
|
loadLanguageAsync,
|
||||||
|
}
|
108
easytier-web/frontend-lib/src/modules/utils.ts
Normal file
108
easytier-web/frontend-lib/src/modules/utils.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { IPv4, IPv6 } from 'ip-num/IPNumber'
|
||||||
|
import { Ipv4Addr, Ipv4Inet, Ipv6Addr } from '../types/network'
|
||||||
|
|
||||||
|
export function ipv4ToString(ip: Ipv4Addr) {
|
||||||
|
return IPv4.fromNumber(ip.addr).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ipv4InetToString(ip: Ipv4Inet | undefined) {
|
||||||
|
if (ip?.address === undefined) {
|
||||||
|
return 'undefined'
|
||||||
|
}
|
||||||
|
return `${ipv4ToString(ip.address)}/${ip.network_length}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ipv6ToString(ip: Ipv6Addr) {
|
||||||
|
return IPv6.fromBigInt(
|
||||||
|
(BigInt(ip.part1) << BigInt(96))
|
||||||
|
+ (BigInt(ip.part2) << BigInt(64))
|
||||||
|
+ (BigInt(ip.part3) << BigInt(32))
|
||||||
|
+ 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);
|
||||||
|
}
|
||||||
|
}
|
54
easytier-web/frontend-lib/src/style.css
Normal file
54
easytier-web/frontend-lib/src/style.css
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
@import 'primeicons/primeicons.css';
|
||||||
|
@import 'floating-vue/dist/style.css';
|
||||||
|
|
||||||
|
.frontend-lib {
|
||||||
|
|
||||||
|
@layer tailwind-base, primevue, tailwind-utilities;
|
||||||
|
|
||||||
|
@layer tailwind-base {
|
||||||
|
@tailwind base;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer tailwind-utilities {
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 24px;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color: #0f0f0f;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--surface-card);
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #0000005d;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,9 +1,9 @@
|
|||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
export enum NetworkingMethod {
|
export enum NetworkingMethod {
|
||||||
PublicServer = 'PublicServer',
|
PublicServer = 0,
|
||||||
Manual = 'Manual',
|
Manual = 1,
|
||||||
Standalone = 'Standalone',
|
Standalone = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NetworkConfig {
|
export interface NetworkConfig {
|
||||||
@@ -11,6 +11,7 @@ export interface NetworkConfig {
|
|||||||
|
|
||||||
dhcp: boolean
|
dhcp: boolean
|
||||||
virtual_ipv4: string
|
virtual_ipv4: string
|
||||||
|
network_length: number
|
||||||
hostname?: string
|
hostname?: string
|
||||||
network_name: string
|
network_name: string
|
||||||
network_secret: string
|
network_secret: string
|
||||||
@@ -42,6 +43,7 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
|||||||
|
|
||||||
dhcp: true,
|
dhcp: true,
|
||||||
virtual_ipv4: '',
|
virtual_ipv4: '',
|
||||||
|
network_length: 24,
|
||||||
network_name: 'easytier',
|
network_name: 'easytier',
|
||||||
network_secret: '',
|
network_secret: '',
|
||||||
|
|
||||||
@@ -82,8 +84,7 @@ export interface NetworkInstance {
|
|||||||
export interface NetworkInstanceRunningInfo {
|
export interface NetworkInstanceRunningInfo {
|
||||||
dev_name: string
|
dev_name: string
|
||||||
my_node_info: NodeInfo
|
my_node_info: NodeInfo
|
||||||
events: Record<string, any>
|
events: Array<string>,
|
||||||
node_info: NodeInfo
|
|
||||||
routes: Route[]
|
routes: Route[]
|
||||||
peers: PeerInfo[]
|
peers: PeerInfo[]
|
||||||
peer_route_pairs: PeerRoutePair[]
|
peer_route_pairs: PeerRoutePair[]
|
||||||
@@ -95,6 +96,11 @@ 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
|
||||||
@@ -102,8 +108,12 @@ export interface Ipv6Addr {
|
|||||||
part4: number
|
part4: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Url {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface NodeInfo {
|
export interface NodeInfo {
|
||||||
virtual_ipv4: string
|
virtual_ipv4: Ipv4Inet,
|
||||||
hostname: string
|
hostname: string
|
||||||
version: string
|
version: string
|
||||||
ips: {
|
ips: {
|
||||||
@@ -125,7 +135,7 @@ export interface NodeInfo {
|
|||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
stun_info: StunInfo
|
stun_info: StunInfo
|
||||||
listeners: string[]
|
listeners: Url[]
|
||||||
vpn_portal_cfg?: string
|
vpn_portal_cfg?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +147,7 @@ export interface StunInfo {
|
|||||||
|
|
||||||
export interface Route {
|
export interface Route {
|
||||||
peer_id: number
|
peer_id: number
|
||||||
ipv4_addr: string
|
ipv4_addr: Ipv4Inet | string | null
|
||||||
next_hop_peer_id: number
|
next_hop_peer_id: number
|
||||||
cost: number
|
cost: number
|
||||||
proxy_cidrs: string[]
|
proxy_cidrs: string[]
|
||||||
@@ -155,6 +165,7 @@ export interface PeerInfo {
|
|||||||
export interface PeerConnInfo {
|
export interface PeerConnInfo {
|
||||||
conn_id: string
|
conn_id: string
|
||||||
my_peer_id: number
|
my_peer_id: number
|
||||||
|
is_client: boolean
|
||||||
peer_id: number
|
peer_id: number
|
||||||
features: string[]
|
features: string[]
|
||||||
tunnel?: TunnelInfo
|
tunnel?: TunnelInfo
|
||||||
@@ -180,3 +191,28 @@ export interface PeerConnStats {
|
|||||||
tx_packets: number
|
tx_packets: number
|
||||||
latency_us: 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
|
||||||
|
}
|
1
easytier-web/frontend-lib/src/vite-env.d.ts
vendored
Normal file
1
easytier-web/frontend-lib/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
11
easytier-web/frontend-lib/tailwind.config.js
Normal file
11
easytier-web/frontend-lib/tailwind.config.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
'./index.html',
|
||||||
|
'./src/**/*.{vue,js,ts,jsx,tsx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [require('tailwindcss-primeui')],
|
||||||
|
}
|
31
easytier-web/frontend-lib/tsconfig.app.json
Normal file
31
easytier-web/frontend-lib/tsconfig.app.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"types": [
|
||||||
|
"@modyfi/vite-plugin-yaml/modules"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||||
|
}
|
7
easytier-web/frontend-lib/tsconfig.json
Normal file
7
easytier-web/frontend-lib/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
24
easytier-web/frontend-lib/tsconfig.node.json
Normal file
24
easytier-web/frontend-lib/tsconfig.node.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
38
easytier-web/frontend-lib/vite.config.ts
Normal file
38
easytier-web/frontend-lib/vite.config.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { resolve } from 'path'
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import dts from "vite-plugin-dts"
|
||||||
|
import ViteYaml from '@modyfi/vite-plugin-yaml';
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue(), dts({
|
||||||
|
tsconfigPath: './tsconfig.app.json',
|
||||||
|
}), ViteYaml()],
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
// Could also be a dictionary or array of multiple entry points
|
||||||
|
entry: resolve(__dirname, 'src/index.ts'),
|
||||||
|
name: 'easytier-frontend-lib',
|
||||||
|
// the proper extensions will be added
|
||||||
|
fileName: 'easytier-frontend-lib',
|
||||||
|
formats: ["es", "umd", "cjs"],
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
main: resolve(__dirname, "src/easytier-frontend-lib.ts")
|
||||||
|
},
|
||||||
|
// make sure to externalize deps that shouldn't be bundled
|
||||||
|
// into your library
|
||||||
|
external: ['vue'],
|
||||||
|
output: {
|
||||||
|
// Provide global variables to use in the UMD build
|
||||||
|
// for externalized deps
|
||||||
|
globals: {
|
||||||
|
vue: 'Vue',
|
||||||
|
},
|
||||||
|
exports: "named"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
24
easytier-web/frontend/.gitignore
vendored
Normal file
24
easytier-web/frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
5
easytier-web/frontend/README.md
Normal file
5
easytier-web/frontend/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Vue 3 + TypeScript + Vite
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||||
|
|
||||||
|
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
13
easytier-web/frontend/index.html
Normal file
13
easytier-web/frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/png" href="/easytier.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>EasyTier Dashboard</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
32
easytier-web/frontend/package.json
Normal file
32
easytier-web/frontend/package.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "easytier-frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc -b && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@primevue/themes": "^4.2.1",
|
||||||
|
"aura": "link:@primevue/themes/aura",
|
||||||
|
"axios": "^1.7.7",
|
||||||
|
"easytier-frontend-lib": "workspace:*",
|
||||||
|
"primevue": "^4.2.1",
|
||||||
|
"tailwindcss-primeui": "^0.3.4",
|
||||||
|
"vue": "^3.5.12",
|
||||||
|
"vue-router": "4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.8.6",
|
||||||
|
"@vitejs/plugin-vue": "^5.1.4",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"postcss": "^8.4.47",
|
||||||
|
"tailwindcss": "^3.4.14",
|
||||||
|
"typescript": "~5.6.2",
|
||||||
|
"vite": "^5.4.10",
|
||||||
|
"vite-plugin-singlefile": "^2.0.3",
|
||||||
|
"vue-tsc": "^2.1.10"
|
||||||
|
}
|
||||||
|
}
|
6
easytier-web/frontend/postcss.config.js
Normal file
6
easytier-web/frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
}
|
||||||
|
}
|
BIN
easytier-web/frontend/public/easytier.png
Normal file
BIN
easytier-web/frontend/public/easytier.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
27
easytier-web/frontend/src/App.vue
Normal file
27
easytier-web/frontend/src/App.vue
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import { I18nUtils } from 'easytier-frontend-lib'
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
import { Toast, DynamicDialog } from 'primevue';
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await I18nUtils.loadLanguageAsync('cn')
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- https://flowbite.com/docs/components/sidebar/#sidebar-with-navbar -->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Toast position="bottom-right" />
|
||||||
|
<DynamicDialog />
|
||||||
|
|
||||||
|
<RouterView />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
button {
|
||||||
|
text-align: left;
|
||||||
|
justify-content: left;
|
||||||
|
}
|
||||||
|
</style>
|
BIN
easytier-web/frontend/src/assets/easytier.png
Normal file
BIN
easytier-web/frontend/src/assets/easytier.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
33
easytier-web/frontend/src/components/ChangePassword.vue
Normal file
33
easytier-web/frontend/src/components/ChangePassword.vue
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<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>
|
65
easytier-web/frontend/src/components/Dashboard.vue
Normal file
65
easytier-web/frontend/src/components/Dashboard.vue
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<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>
|
110
easytier-web/frontend/src/components/DeviceList.vue
Normal file
110
easytier-web/frontend/src/components/DeviceList.vue
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||||
|
import { Button, Column, DataTable, Drawer, ProgressSpinner, useToast } from 'primevue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { Api, Utils } from 'easytier-frontend-lib';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
api: Api.ApiClient,
|
||||||
|
});
|
||||||
|
|
||||||
|
const api = props.api;
|
||||||
|
|
||||||
|
const deviceList = ref<Array<Utils.DeviceInfo> | undefined>(undefined);
|
||||||
|
|
||||||
|
const selectedDeviceId = computed<string | undefined>(() => route.params.deviceId as string);
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const loadDevices = async () => {
|
||||||
|
const resp = await api?.list_machines();
|
||||||
|
let devices: Array<Utils.DeviceInfo> = [];
|
||||||
|
for (const device of (resp || [])) {
|
||||||
|
devices.push({
|
||||||
|
hostname: device.info?.hostname,
|
||||||
|
public_ip: device.client_url,
|
||||||
|
running_network_instances: device.info?.running_network_instances.map((instance: any) => Utils.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: Utils.UuidToStr(device.info?.machine_id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.debug("device list", deviceList.value);
|
||||||
|
deviceList.value = devices;
|
||||||
|
};
|
||||||
|
|
||||||
|
const periodFunc = new Utils.PeriodicTask(async () => {
|
||||||
|
try {
|
||||||
|
await loadDevices();
|
||||||
|
} catch (e) {
|
||||||
|
toast.add({ severity: 'error', summary: 'Load Device List Failed', detail: e, life: 2000 });
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
periodFunc.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
periodFunc.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
const deviceManageVisible = computed<boolean>({
|
||||||
|
get: () => !!selectedDeviceId.value,
|
||||||
|
set: (value) => {
|
||||||
|
if (!value) {
|
||||||
|
router.push({ name: 'deviceList', params: { deviceId: undefined } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedDeviceHostname = computed<string | undefined>(() => {
|
||||||
|
return deviceList.value?.find((device) => device.machine_id === selectedDeviceId.value)?.hostname;
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="deviceList === undefined" class="w-full flex justify-center">
|
||||||
|
<ProgressSpinner />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable :value="deviceList" tableStyle="min-width: 50rem" :metaKeySelection="true" sortField="hostname"
|
||||||
|
:sortOrder="-1" v-if="deviceList !== undefined">
|
||||||
|
<template #header>
|
||||||
|
<div class="text-xl font-bold">Device List</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<Column field="hostname" header="Hostname" sortable style="width: 180px"></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="report_time" header="Report Time" sortable style="width: 150px"></Column>
|
||||||
|
<Column field="easytier_version" header="EasyTier Version" sortable style="width: 150px"></Column>
|
||||||
|
<Column class="w-24 !text-end">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<Button icon="pi pi-cog"
|
||||||
|
@click="router.push({ name: 'deviceManagement', params: { deviceId: data.machine_id, instanceId: data.running_network_instances[0] } })"
|
||||||
|
severity="secondary" rounded></Button>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<Button icon="pi pi-refresh" label="Reload" severity="info" @click="loadDevices" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</DataTable>
|
||||||
|
|
||||||
|
<Drawer v-model:visible="deviceManageVisible" :header="`Manage ${selectedDeviceHostname}`" position="right"
|
||||||
|
class="w-1/2 min-w-96">
|
||||||
|
<RouterView v-slot="{ Component }">
|
||||||
|
<component :is="Component" :api="api" :deviceList="deviceList" @update="loadDevices" />
|
||||||
|
</RouterView>
|
||||||
|
</Drawer>
|
||||||
|
</template>
|
197
easytier-web/frontend/src/components/DeviceManagement.vue
Normal file
197
easytier-web/frontend/src/components/DeviceManagement.vue
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
<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>
|
122
easytier-web/frontend/src/components/Login.vue
Normal file
122
easytier-web/frontend/src/components/Login.vue
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<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 = 'https://config-server.easytier.cn'
|
||||||
|
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>
|
||||||
|
<div class="flex items-center justify-center min-h-screen">
|
||||||
|
<Card class="w-full max-w-md p-6">
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-2xl font-semibold text-center">{{ isRegistering ? 'Register' : 'Login' }}
|
||||||
|
</h2>
|
||||||
|
</template>
|
||||||
|
<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">
|
||||||
|
<div class="p-field">
|
||||||
|
<label for="username" class="block text-sm font-medium">Username</label>
|
||||||
|
<InputText id="username" v-model="username" required class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="p-field">
|
||||||
|
<label for="password" class="block text-sm font-medium">Password</label>
|
||||||
|
<Password id="password" v-model="password" required toggleMask :feedback="false" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<Button label="Login" type="submit" class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<Button label="Register" type="button" class="w-full"
|
||||||
|
@click="$router.replace({ name: 'register' })" severity="secondary" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form v-else @submit.prevent="onRegister" class="space-y-4">
|
||||||
|
<div class="p-field">
|
||||||
|
<label for="register-username" class="block text-sm font-medium">Username</label>
|
||||||
|
<InputText id="register-username" v-model="registerUsername" required class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="p-field">
|
||||||
|
<label for="register-password" class="block text-sm font-medium">Password</label>
|
||||||
|
<Password id="register-password" v-model="registerPassword" required toggleMask
|
||||||
|
:feedback="false" class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="p-field">
|
||||||
|
<label for="captcha" class="block text-sm font-medium">Captcha</label>
|
||||||
|
<InputText id="captcha" v-model="captcha" required class="w-full" />
|
||||||
|
<img :src="captchaSrc" alt="Captcha" class="mt-2 mb-2" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<Button label="Register" type="submit" class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<Button label="Back to Login" type="button" class="w-full"
|
||||||
|
@click="$router.replace({ name: 'login' })" severity="secondary" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
173
easytier-web/frontend/src/components/MainPage.vue
Normal file
173
easytier-web/frontend/src/components/MainPage.vue
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
<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>
|
90
easytier-web/frontend/src/main.ts
Normal file
90
easytier-web/frontend/src/main.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import './style.css'
|
||||||
|
import 'easytier-frontend-lib/style.css'
|
||||||
|
import App from './App.vue'
|
||||||
|
import EasytierFrontendLib from 'easytier-frontend-lib'
|
||||||
|
import PrimeVue from 'primevue/config'
|
||||||
|
import Aura from '@primevue/themes/aura'
|
||||||
|
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,
|
||||||
|
{
|
||||||
|
theme: {
|
||||||
|
preset: Aura,
|
||||||
|
options: {
|
||||||
|
prefix: 'p',
|
||||||
|
darkModeSelector: 'system',
|
||||||
|
cssLayer: {
|
||||||
|
name: 'primevue',
|
||||||
|
order: 'tailwind-base, primevue, tailwind-utilities'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).use(ToastService as any).use(DialogService as any).use(router).use(ConfirmationService as any).use(EasytierFrontendLib).mount('#app')
|
33
easytier-web/frontend/src/style.css
Normal file
33
easytier-web/frontend/src/style.css
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
@layer tailwind-base, primevue, tailwind-utilities;
|
||||||
|
|
||||||
|
@layer tailwind-base {
|
||||||
|
@tailwind base;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer tailwind-utilities {
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-password {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-password>input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 24px;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color: #0f0f0f;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
}
|
1
easytier-web/frontend/src/vite-env.d.ts
vendored
Normal file
1
easytier-web/frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
11
easytier-web/frontend/tailwind.config.js
Normal file
11
easytier-web/frontend/tailwind.config.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
'./index.html',
|
||||||
|
'./src/**/*.{vue,js,ts,jsx,tsx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [require('tailwindcss-primeui')],
|
||||||
|
}
|
26
easytier-web/frontend/tsconfig.app.json
Normal file
26
easytier-web/frontend/tsconfig.app.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "../frontend-lib/src/modules/api.ts"]
|
||||||
|
}
|
7
easytier-web/frontend/tsconfig.json
Normal file
7
easytier-web/frontend/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
24
easytier-web/frontend/tsconfig.node.json
Normal file
24
easytier-web/frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
9
easytier-web/frontend/vite.config.ts
Normal file
9
easytier-web/frontend/vite.config.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import { viteSingleFile } from "vite-plugin-singlefile"
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
base: '',
|
||||||
|
plugins: [vue(), viteSingleFile()],
|
||||||
|
})
|
85
easytier-web/migrations/20241026_init.sql
Normal file
85
easytier-web/migrations/20241026_init.sql
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
-- # 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')
|
||||||
|
);
|
BIN
easytier-web/resources/robot.ttf
Normal file
BIN
easytier-web/resources/robot.ttf
Normal file
Binary file not shown.
152
easytier-web/src/client_manager/mod.rs
Normal file
152
easytier-web/src/client_manager/mod.rs
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
pub mod session;
|
||||||
|
pub mod storage;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use dashmap::DashMap;
|
||||||
|
use easytier::{
|
||||||
|
common::scoped_task::ScopedTask, proto::web::HeartbeatRequest, tunnel::TunnelListener,
|
||||||
|
};
|
||||||
|
use session::Session;
|
||||||
|
use storage::{Storage, StorageToken};
|
||||||
|
|
||||||
|
use crate::db::Db;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ClientManager {
|
||||||
|
accept_task: Option<ScopedTask<()>>,
|
||||||
|
clear_task: Option<ScopedTask<()>>,
|
||||||
|
|
||||||
|
client_sessions: Arc<DashMap<url::Url, Arc<Session>>>,
|
||||||
|
storage: Storage,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientManager {
|
||||||
|
pub fn new(db: Db) -> Self {
|
||||||
|
ClientManager {
|
||||||
|
accept_task: None,
|
||||||
|
clear_task: None,
|
||||||
|
|
||||||
|
client_sessions: Arc::new(DashMap::new()),
|
||||||
|
storage: Storage::new(db),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn serve<L: TunnelListener + 'static>(
|
||||||
|
&mut self,
|
||||||
|
mut listener: L,
|
||||||
|
) -> Result<(), anyhow::Error> {
|
||||||
|
listener.listen().await?;
|
||||||
|
|
||||||
|
let sessions = self.client_sessions.clone();
|
||||||
|
let storage = self.storage.weak_ref();
|
||||||
|
let task = tokio::spawn(async move {
|
||||||
|
while let Ok(tunnel) = listener.accept().await {
|
||||||
|
let info = tunnel.info().unwrap();
|
||||||
|
let client_url: url::Url = info.remote_addr.unwrap().into();
|
||||||
|
println!("New session from {:?}", tunnel.info());
|
||||||
|
let mut session = Session::new(storage.clone(), client_url.clone());
|
||||||
|
session.serve(tunnel).await;
|
||||||
|
sessions.insert(client_url, Arc::new(session));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.accept_task = Some(ScopedTask::from(task));
|
||||||
|
|
||||||
|
let sessions = self.client_sessions.clone();
|
||||||
|
let task = tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(15)).await;
|
||||||
|
sessions.retain(|_, session| session.is_running());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.clear_task = Some(ScopedTask::from(task));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_running(&self) -> bool {
|
||||||
|
self.accept_task.is_some() && self.clear_task.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_sessions(&self) -> Vec<StorageToken> {
|
||||||
|
let sessions = self
|
||||||
|
.client_sessions
|
||||||
|
.iter()
|
||||||
|
.map(|item| item.value().clone())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let mut ret: Vec<StorageToken> = vec![];
|
||||||
|
for s in sessions {
|
||||||
|
if let Some(t) = s.get_token().await {
|
||||||
|
ret.push(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_session_by_machine_id(&self, machine_id: &uuid::Uuid) -> Option<Arc<Session>> {
|
||||||
|
let c_url = self.storage.get_client_url_by_machine_id(machine_id)?;
|
||||||
|
self.client_sessions
|
||||||
|
.get(&c_url)
|
||||||
|
.map(|item| item.value().clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_machine_by_token(&self, token: String) -> Vec<url::Url> {
|
||||||
|
self.storage.list_token_clients(&token)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_heartbeat_requests(&self, client_url: &url::Url) -> Option<HeartbeatRequest> {
|
||||||
|
let s = self.client_sessions.get(client_url)?.clone();
|
||||||
|
s.data().read().await.req()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn db(&self) -> &Db {
|
||||||
|
self.storage.db()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use easytier::{
|
||||||
|
tunnel::{
|
||||||
|
common::tests::wait_for_condition,
|
||||||
|
udp::{UdpTunnelConnector, UdpTunnelListener},
|
||||||
|
},
|
||||||
|
web_client::WebClient,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{client_manager::ClientManager, db::Db};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_client() {
|
||||||
|
let listener = UdpTunnelListener::new("udp://0.0.0.0:54333".parse().unwrap());
|
||||||
|
let mut mgr = ClientManager::new(Db::memory_db().await);
|
||||||
|
mgr.serve(Box::new(listener)).await.unwrap();
|
||||||
|
|
||||||
|
let connector = UdpTunnelConnector::new("udp://127.0.0.1:54333".parse().unwrap());
|
||||||
|
let _c = WebClient::new(connector, "test");
|
||||||
|
|
||||||
|
wait_for_condition(
|
||||||
|
|| async { mgr.client_sessions.len() == 1 },
|
||||||
|
Duration::from_secs(6),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut a = mgr
|
||||||
|
.client_sessions
|
||||||
|
.iter()
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.data()
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.heartbeat_waiter();
|
||||||
|
let req = a.recv().await.unwrap();
|
||||||
|
println!("{:?}", req);
|
||||||
|
println!("{:?}", mgr);
|
||||||
|
}
|
||||||
|
}
|
264
easytier-web/src/client_manager/session.rs
Normal file
264
easytier-web/src/client_manager/session.rs
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
use std::{fmt::Debug, sync::Arc};
|
||||||
|
|
||||||
|
use easytier::{
|
||||||
|
common::scoped_task::ScopedTask,
|
||||||
|
proto::{
|
||||||
|
rpc_impl::bidirect::BidirectRpcManager,
|
||||||
|
rpc_types::{self, controller::BaseController},
|
||||||
|
web::{
|
||||||
|
HeartbeatRequest, HeartbeatResponse, NetworkConfig, RunNetworkInstanceRequest,
|
||||||
|
WebClientService, WebClientServiceClientFactory, WebServerService,
|
||||||
|
WebServerServiceServer,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tunnel::Tunnel,
|
||||||
|
};
|
||||||
|
use tokio::sync::{broadcast, RwLock};
|
||||||
|
|
||||||
|
use super::storage::{Storage, StorageToken, WeakRefStorage};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SessionData {
|
||||||
|
storage: WeakRefStorage,
|
||||||
|
client_url: url::Url,
|
||||||
|
|
||||||
|
storage_token: Option<StorageToken>,
|
||||||
|
notifier: broadcast::Sender<HeartbeatRequest>,
|
||||||
|
req: Option<HeartbeatRequest>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SessionData {
|
||||||
|
fn new(storage: WeakRefStorage, client_url: url::Url) -> Self {
|
||||||
|
let (tx, _rx1) = broadcast::channel(2);
|
||||||
|
|
||||||
|
SessionData {
|
||||||
|
storage,
|
||||||
|
client_url,
|
||||||
|
storage_token: None,
|
||||||
|
notifier: tx,
|
||||||
|
req: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn req(&self) -> Option<HeartbeatRequest> {
|
||||||
|
self.req.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn heartbeat_waiter(&self) -> broadcast::Receiver<HeartbeatRequest> {
|
||||||
|
self.notifier.subscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for SessionData {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Ok(storage) = Storage::try_from(self.storage.clone()) {
|
||||||
|
if let Some(token) = self.storage_token.as_ref() {
|
||||||
|
storage.remove_client(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type SharedSessionData = Arc<RwLock<SessionData>>;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct SessionRpcService {
|
||||||
|
data: SharedSessionData,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl WebServerService for SessionRpcService {
|
||||||
|
type Controller = BaseController;
|
||||||
|
|
||||||
|
async fn heartbeat(
|
||||||
|
&self,
|
||||||
|
_: BaseController,
|
||||||
|
req: HeartbeatRequest,
|
||||||
|
) -> rpc_types::error::Result<HeartbeatResponse> {
|
||||||
|
let mut data = self.data.write().await;
|
||||||
|
if data.req.replace(req.clone()).is_none() {
|
||||||
|
assert!(data.storage_token.is_none());
|
||||||
|
data.storage_token = Some(StorageToken {
|
||||||
|
token: req.user_token.clone().into(),
|
||||||
|
client_url: data.client_url.clone(),
|
||||||
|
machine_id: req
|
||||||
|
.machine_id
|
||||||
|
.clone()
|
||||||
|
.map(Into::into)
|
||||||
|
.unwrap_or(uuid::Uuid::new_v4()),
|
||||||
|
});
|
||||||
|
if let Ok(storage) = Storage::try_from(data.storage.clone()) {
|
||||||
|
storage.add_client(data.storage_token.as_ref().unwrap().clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = data.notifier.send(req);
|
||||||
|
Ok(HeartbeatResponse {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Session {
|
||||||
|
rpc_mgr: BidirectRpcManager,
|
||||||
|
|
||||||
|
data: SharedSessionData,
|
||||||
|
|
||||||
|
run_network_on_start_task: Option<ScopedTask<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for Session {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("Session").field("data", &self.data).finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SessionRpcClient = Box<dyn WebClientService<Controller = BaseController> + Send>;
|
||||||
|
|
||||||
|
impl Session {
|
||||||
|
pub fn new(storage: WeakRefStorage, client_url: url::Url) -> Self {
|
||||||
|
let session_data = SessionData::new(storage, client_url);
|
||||||
|
let data = Arc::new(RwLock::new(session_data));
|
||||||
|
|
||||||
|
let rpc_mgr =
|
||||||
|
BidirectRpcManager::new().set_rx_timeout(Some(std::time::Duration::from_secs(30)));
|
||||||
|
|
||||||
|
rpc_mgr.rpc_server().registry().register(
|
||||||
|
WebServerServiceServer::new(SessionRpcService { data: data.clone() }),
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
Session {
|
||||||
|
rpc_mgr,
|
||||||
|
data,
|
||||||
|
run_network_on_start_task: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn serve(&mut self, tunnel: Box<dyn Tunnel>) {
|
||||||
|
self.rpc_mgr.run_with_tunnel(tunnel);
|
||||||
|
|
||||||
|
let data = self.data.read().await;
|
||||||
|
self.run_network_on_start_task.replace(
|
||||||
|
tokio::spawn(Self::run_network_on_start(
|
||||||
|
data.heartbeat_waiter(),
|
||||||
|
data.storage.clone(),
|
||||||
|
self.scoped_rpc_client(),
|
||||||
|
))
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_network_on_start(
|
||||||
|
mut heartbeat_waiter: broadcast::Receiver<HeartbeatRequest>,
|
||||||
|
storage: WeakRefStorage,
|
||||||
|
rpc_client: SessionRpcClient,
|
||||||
|
) {
|
||||||
|
loop {
|
||||||
|
heartbeat_waiter = heartbeat_waiter.resubscribe();
|
||||||
|
let req = heartbeat_waiter.recv().await;
|
||||||
|
if req.is_err() {
|
||||||
|
tracing::error!(
|
||||||
|
"Failed to receive heartbeat request, error: {:?}",
|
||||||
|
req.err()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
.running_network_instances
|
||||||
|
.iter()
|
||||||
|
.map(|x| x.to_string())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let Some(storage) = storage.upgrade() else {
|
||||||
|
tracing::error!("Failed to get storage");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_id = match storage
|
||||||
|
.db
|
||||||
|
.get_user_id_by_token(req.user_token.clone())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Some(user_id)) => user_id,
|
||||||
|
Ok(None) => {
|
||||||
|
tracing::info!("User not found by token: {:?}", req.user_token);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to get user id by token, error: {:?}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let local_configs = match storage
|
||||||
|
.db
|
||||||
|
.list_network_configs(user_id, Some(req.machine_id.unwrap().into()), true)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(configs) => configs,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to list network configs, error: {:?}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut has_failed = false;
|
||||||
|
|
||||||
|
for c in local_configs {
|
||||||
|
if running_inst_ids.contains(&c.network_instance_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let ret = rpc_client
|
||||||
|
.run_network_instance(
|
||||||
|
BaseController::default(),
|
||||||
|
RunNetworkInstanceRequest {
|
||||||
|
inst_id: Some(c.network_instance_id.clone().into()),
|
||||||
|
config: Some(
|
||||||
|
serde_json::from_str::<NetworkConfig>(&c.network_config).unwrap(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
tracing::info!(
|
||||||
|
?user_id,
|
||||||
|
"Run network instance: {:?}, user_token: {:?}",
|
||||||
|
ret,
|
||||||
|
req.user_token
|
||||||
|
);
|
||||||
|
|
||||||
|
has_failed |= ret.is_err();
|
||||||
|
}
|
||||||
|
|
||||||
|
if !has_failed {
|
||||||
|
tracing::info!(?req, "All network instances are running");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_running(&self) -> bool {
|
||||||
|
self.rpc_mgr.is_running()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn data(&self) -> SharedSessionData {
|
||||||
|
self.data.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scoped_rpc_client(&self) -> SessionRpcClient {
|
||||||
|
self.rpc_mgr
|
||||||
|
.rpc_client()
|
||||||
|
.scoped_client::<WebClientServiceClientFactory<BaseController>>(1, 1, "".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_token(&self) -> Option<StorageToken> {
|
||||||
|
self.data.read().await.storage_token.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_heartbeat_req(&self) -> Option<HeartbeatRequest> {
|
||||||
|
self.data.read().await.req()
|
||||||
|
}
|
||||||
|
}
|
96
easytier-web/src/client_manager/storage.rs
Normal file
96
easytier-web/src/client_manager/storage.rs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
use std::sync::{Arc, Weak};
|
||||||
|
|
||||||
|
use dashmap::{DashMap, DashSet};
|
||||||
|
|
||||||
|
use crate::db::Db;
|
||||||
|
|
||||||
|
// use this to maintain Storage
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct StorageToken {
|
||||||
|
pub token: String,
|
||||||
|
pub client_url: url::Url,
|
||||||
|
pub machine_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct StorageInner {
|
||||||
|
// some map for indexing
|
||||||
|
pub token_clients_map: DashMap<String, DashSet<url::Url>>,
|
||||||
|
pub machine_client_url_map: DashMap<uuid::Uuid, DashSet<url::Url>>,
|
||||||
|
pub db: Db,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Storage(Arc<StorageInner>);
|
||||||
|
pub type WeakRefStorage = Weak<StorageInner>;
|
||||||
|
|
||||||
|
impl TryFrom<WeakRefStorage> for Storage {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(weak: Weak<StorageInner>) -> Result<Self, Self::Error> {
|
||||||
|
weak.upgrade().map(|inner| Storage(inner)).ok_or(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Storage {
|
||||||
|
pub fn new(db: Db) -> Self {
|
||||||
|
Storage(Arc::new(StorageInner {
|
||||||
|
token_clients_map: DashMap::new(),
|
||||||
|
machine_client_url_map: DashMap::new(),
|
||||||
|
db,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_client(&self, stoken: StorageToken) {
|
||||||
|
let inner = self
|
||||||
|
.0
|
||||||
|
.token_clients_map
|
||||||
|
.entry(stoken.token)
|
||||||
|
.or_insert_with(DashSet::new);
|
||||||
|
inner.insert(stoken.client_url.clone());
|
||||||
|
|
||||||
|
self.0
|
||||||
|
.machine_client_url_map
|
||||||
|
.entry(stoken.machine_id)
|
||||||
|
.or_insert_with(DashSet::new)
|
||||||
|
.insert(stoken.client_url.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_client(&self, stoken: &StorageToken) {
|
||||||
|
self.0.token_clients_map.remove_if(&stoken.token, |_, set| {
|
||||||
|
set.remove(&stoken.client_url);
|
||||||
|
set.is_empty()
|
||||||
|
});
|
||||||
|
|
||||||
|
self.0
|
||||||
|
.machine_client_url_map
|
||||||
|
.remove_if(&stoken.machine_id, |_, set| {
|
||||||
|
set.remove(&stoken.client_url);
|
||||||
|
set.is_empty()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn weak_ref(&self) -> WeakRefStorage {
|
||||||
|
Arc::downgrade(&self.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_client_url_by_machine_id(&self, machine_id: &uuid::Uuid) -> Option<url::Url> {
|
||||||
|
self.0
|
||||||
|
.machine_client_url_map
|
||||||
|
.get(&machine_id)
|
||||||
|
.map(|url| url.iter().next().map(|url| url.clone()))
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_token_clients(&self, token: &str) -> Vec<url::Url> {
|
||||||
|
self.0
|
||||||
|
.token_clients_map
|
||||||
|
.get(token)
|
||||||
|
.map(|set| set.iter().map(|url| url.clone()).collect())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn db(&self) -> &Db {
|
||||||
|
&self.0.db
|
||||||
|
}
|
||||||
|
}
|
35
easytier-web/src/db/entity/groups.rs
Normal file
35
easytier-web/src/db/entity/groups.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||||
|
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||||
|
#[sea_orm(table_name = "groups")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key)]
|
||||||
|
pub id: i32,
|
||||||
|
#[sea_orm(unique)]
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {
|
||||||
|
#[sea_orm(has_many = "super::groups_permissions::Entity")]
|
||||||
|
GroupsPermissions,
|
||||||
|
#[sea_orm(has_many = "super::users_groups::Entity")]
|
||||||
|
UsersGroups,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::groups_permissions::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::GroupsPermissions.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::users_groups::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::UsersGroups.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
47
easytier-web/src/db/entity/groups_permissions.rs
Normal file
47
easytier-web/src/db/entity/groups_permissions.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||||
|
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||||
|
#[sea_orm(table_name = "groups_permissions")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key)]
|
||||||
|
pub id: i32,
|
||||||
|
pub group_id: i32,
|
||||||
|
pub permission_id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "super::groups::Entity",
|
||||||
|
from = "Column::GroupId",
|
||||||
|
to = "super::groups::Column::Id",
|
||||||
|
on_update = "Cascade",
|
||||||
|
on_delete = "Cascade"
|
||||||
|
)]
|
||||||
|
Groups,
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "super::permissions::Entity",
|
||||||
|
from = "Column::PermissionId",
|
||||||
|
to = "super::permissions::Column::Id",
|
||||||
|
on_update = "Cascade",
|
||||||
|
on_delete = "Cascade"
|
||||||
|
)]
|
||||||
|
Permissions,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::groups::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Groups.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::permissions::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Permissions.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
11
easytier-web/src/db/entity/mod.rs
Normal file
11
easytier-web/src/db/entity/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||||
|
|
||||||
|
pub mod prelude;
|
||||||
|
|
||||||
|
pub mod groups;
|
||||||
|
pub mod groups_permissions;
|
||||||
|
pub mod permissions;
|
||||||
|
pub mod tower_sessions;
|
||||||
|
pub mod user_running_network_configs;
|
||||||
|
pub mod users;
|
||||||
|
pub mod users_groups;
|
27
easytier-web/src/db/entity/permissions.rs
Normal file
27
easytier-web/src/db/entity/permissions.rs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||||
|
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||||
|
#[sea_orm(table_name = "permissions")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key)]
|
||||||
|
pub id: i32,
|
||||||
|
#[sea_orm(unique)]
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {
|
||||||
|
#[sea_orm(has_many = "super::groups_permissions::Entity")]
|
||||||
|
GroupsPermissions,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::groups_permissions::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::GroupsPermissions.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
9
easytier-web/src/db/entity/prelude.rs
Normal file
9
easytier-web/src/db/entity/prelude.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||||
|
|
||||||
|
pub use super::groups::Entity as Groups;
|
||||||
|
pub use super::groups_permissions::Entity as GroupsPermissions;
|
||||||
|
pub use super::permissions::Entity as Permissions;
|
||||||
|
pub use super::tower_sessions::Entity as TowerSessions;
|
||||||
|
pub use super::user_running_network_configs::Entity as UserRunningNetworkConfigs;
|
||||||
|
pub use super::users::Entity as Users;
|
||||||
|
pub use super::users_groups::Entity as UsersGroups;
|
19
easytier-web/src/db/entity/tower_sessions.rs
Normal file
19
easytier-web/src/db/entity/tower_sessions.rs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||||
|
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||||
|
#[sea_orm(table_name = "tower_sessions")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key, auto_increment = false, column_type = "Text")]
|
||||||
|
pub id: String,
|
||||||
|
#[sea_orm(column_type = "Blob")]
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
pub expiry_date: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
41
easytier-web/src/db/entity/user_running_network_configs.rs
Normal file
41
easytier-web/src/db/entity/user_running_network_configs.rs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||||
|
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||||
|
#[sea_orm(table_name = "user_running_network_configs")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key)]
|
||||||
|
pub id: i32,
|
||||||
|
pub user_id: i32,
|
||||||
|
#[sea_orm(column_type = "Text")]
|
||||||
|
pub device_id: String,
|
||||||
|
#[sea_orm(column_type = "Text", unique)]
|
||||||
|
pub network_instance_id: String,
|
||||||
|
#[sea_orm(column_type = "Text")]
|
||||||
|
pub network_config: String,
|
||||||
|
pub disabled: bool,
|
||||||
|
pub create_time: DateTimeWithTimeZone,
|
||||||
|
pub update_time: DateTimeWithTimeZone,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "super::users::Entity",
|
||||||
|
from = "Column::UserId",
|
||||||
|
to = "super::users::Column::Id",
|
||||||
|
on_update = "Cascade",
|
||||||
|
on_delete = "Cascade"
|
||||||
|
)]
|
||||||
|
Users,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::users::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Users.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
36
easytier-web/src/db/entity/users.rs
Normal file
36
easytier-web/src/db/entity/users.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||||
|
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||||
|
#[sea_orm(table_name = "users")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key)]
|
||||||
|
pub id: i32,
|
||||||
|
#[sea_orm(unique)]
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {
|
||||||
|
#[sea_orm(has_many = "super::user_running_network_configs::Entity")]
|
||||||
|
UserRunningNetworkConfigs,
|
||||||
|
#[sea_orm(has_many = "super::users_groups::Entity")]
|
||||||
|
UsersGroups,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::user_running_network_configs::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::UserRunningNetworkConfigs.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::users_groups::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::UsersGroups.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
47
easytier-web/src/db/entity/users_groups.rs
Normal file
47
easytier-web/src/db/entity/users_groups.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||||
|
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||||
|
#[sea_orm(table_name = "users_groups")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key)]
|
||||||
|
pub id: i32,
|
||||||
|
pub user_id: i32,
|
||||||
|
pub group_id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "super::groups::Entity",
|
||||||
|
from = "Column::GroupId",
|
||||||
|
to = "super::groups::Column::Id",
|
||||||
|
on_update = "Cascade",
|
||||||
|
on_delete = "Cascade"
|
||||||
|
)]
|
||||||
|
Groups,
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "super::users::Entity",
|
||||||
|
from = "Column::UserId",
|
||||||
|
to = "super::users::Column::Id",
|
||||||
|
on_update = "Cascade",
|
||||||
|
on_delete = "Cascade"
|
||||||
|
)]
|
||||||
|
Users,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::groups::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Groups.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::users::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Users.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
227
easytier-web/src/db/mod.rs
Normal file
227
easytier-web/src/db/mod.rs
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
// sea-orm-cli generate entity -u sqlite:./et.db -o easytier-web/src/db/entity/ --with-serde both --with-copy-enums
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
pub mod entity;
|
||||||
|
|
||||||
|
use entity::user_running_network_configs;
|
||||||
|
use sea_orm::{
|
||||||
|
sea_query::OnConflict, ColumnTrait as _, DatabaseConnection, DbErr, EntityTrait as _,
|
||||||
|
QueryFilter as _, SqlxSqliteConnector, TransactionTrait as _,
|
||||||
|
};
|
||||||
|
use sea_orm_migration::MigratorTrait as _;
|
||||||
|
use sqlx::{migrate::MigrateDatabase as _, types::chrono, Sqlite, SqlitePool};
|
||||||
|
|
||||||
|
use crate::migrator;
|
||||||
|
|
||||||
|
type UserIdInDb = i32;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Db {
|
||||||
|
db_path: String,
|
||||||
|
db: SqlitePool,
|
||||||
|
orm_db: DatabaseConnection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Db {
|
||||||
|
pub async fn new<T: ToString>(db_path: T) -> anyhow::Result<Self> {
|
||||||
|
let db = Self::prepare_db(db_path.to_string().as_str()).await?;
|
||||||
|
let orm_db = SqlxSqliteConnector::from_sqlx_sqlite_pool(db.clone());
|
||||||
|
migrator::Migrator::up(&orm_db, None).await?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
db_path: db_path.to_string(),
|
||||||
|
db,
|
||||||
|
orm_db,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn memory_db() -> Self {
|
||||||
|
Self::new(":memory:").await.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(ret)]
|
||||||
|
async fn prepare_db(db_path: &str) -> anyhow::Result<SqlitePool> {
|
||||||
|
if !Sqlite::database_exists(db_path).await.unwrap_or(false) {
|
||||||
|
tracing::info!("Database not found, creating a new one");
|
||||||
|
Sqlite::create_database(db_path).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let db = sqlx::pool::PoolOptions::new()
|
||||||
|
.max_lifetime(None)
|
||||||
|
.idle_timeout(None)
|
||||||
|
.connect(db_path)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn inner(&self) -> SqlitePool {
|
||||||
|
self.db.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn orm_db(&self) -> &DatabaseConnection {
|
||||||
|
&self.orm_db
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn insert_or_update_user_network_config<T: ToString>(
|
||||||
|
&self,
|
||||||
|
user_id: UserIdInDb,
|
||||||
|
device_id: uuid::Uuid,
|
||||||
|
network_inst_id: uuid::Uuid,
|
||||||
|
network_config: T,
|
||||||
|
) -> Result<(), DbErr> {
|
||||||
|
let txn = self.orm_db().begin().await?;
|
||||||
|
|
||||||
|
use entity::user_running_network_configs as urnc;
|
||||||
|
|
||||||
|
let on_conflict = OnConflict::column(urnc::Column::NetworkInstanceId)
|
||||||
|
.update_columns([
|
||||||
|
urnc::Column::NetworkConfig,
|
||||||
|
urnc::Column::Disabled,
|
||||||
|
urnc::Column::UpdateTime,
|
||||||
|
])
|
||||||
|
.to_owned();
|
||||||
|
let insert_m = urnc::ActiveModel {
|
||||||
|
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_config: sea_orm::Set(network_config.to_string()),
|
||||||
|
disabled: sea_orm::Set(false),
|
||||||
|
create_time: sea_orm::Set(chrono::Local::now().fixed_offset()),
|
||||||
|
update_time: sea_orm::Set(chrono::Local::now().fixed_offset()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
urnc::Entity::insert(insert_m)
|
||||||
|
.on_conflict(on_conflict)
|
||||||
|
.do_nothing()
|
||||||
|
.exec(&txn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
txn.commit().await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_network_config(
|
||||||
|
&self,
|
||||||
|
user_id: UserIdInDb,
|
||||||
|
network_inst_id: uuid::Uuid,
|
||||||
|
) -> Result<(), DbErr> {
|
||||||
|
use entity::user_running_network_configs as urnc;
|
||||||
|
|
||||||
|
urnc::Entity::delete_many()
|
||||||
|
.filter(urnc::Column::UserId.eq(user_id))
|
||||||
|
.filter(urnc::Column::NetworkInstanceId.eq(network_inst_id.to_string()))
|
||||||
|
.exec(self.orm_db())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_network_configs(
|
||||||
|
&self,
|
||||||
|
user_id: UserIdInDb,
|
||||||
|
device_id: Option<uuid::Uuid>,
|
||||||
|
only_enabled: bool,
|
||||||
|
) -> Result<Vec<user_running_network_configs::Model>, DbErr> {
|
||||||
|
use entity::user_running_network_configs as urnc;
|
||||||
|
|
||||||
|
let configs = urnc::Entity::find().filter(urnc::Column::UserId.eq(user_id));
|
||||||
|
let configs = if only_enabled {
|
||||||
|
configs.filter(urnc::Column::Disabled.eq(false))
|
||||||
|
} else {
|
||||||
|
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?;
|
||||||
|
|
||||||
|
Ok(configs)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_user_id<T: ToString>(
|
||||||
|
&self,
|
||||||
|
user_name: T,
|
||||||
|
) -> Result<Option<UserIdInDb>, DbErr> {
|
||||||
|
use entity::users as u;
|
||||||
|
|
||||||
|
let user = u::Entity::find()
|
||||||
|
.filter(u::Column::Username.eq(user_name.to_string()))
|
||||||
|
.one(self.orm_db())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(user.map(|u| u.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: currently we don't have a token system, so we just use the user name as token
|
||||||
|
pub async fn get_user_id_by_token<T: ToString>(
|
||||||
|
&self,
|
||||||
|
token: T,
|
||||||
|
) -> Result<Option<UserIdInDb>, DbErr> {
|
||||||
|
self.get_user_id(token).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter as _};
|
||||||
|
|
||||||
|
use crate::db::{entity::user_running_network_configs, Db};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_user_network_config_management() {
|
||||||
|
let db = Db::memory_db().await;
|
||||||
|
let user_id = 1;
|
||||||
|
let network_config = "test_config";
|
||||||
|
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)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let result = user_running_network_configs::Entity::find()
|
||||||
|
.filter(user_running_network_configs::Column::UserId.eq(user_id))
|
||||||
|
.one(db.orm_db())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
println!("{:?}", result);
|
||||||
|
assert_eq!(result.network_config, network_config);
|
||||||
|
|
||||||
|
// overwrite the config
|
||||||
|
let network_config = "test_config2";
|
||||||
|
db.insert_or_update_user_network_config(user_id, device_id, inst_id, network_config)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let result2 = user_running_network_configs::Entity::find()
|
||||||
|
.filter(user_running_network_configs::Column::UserId.eq(user_id))
|
||||||
|
.one(db.orm_db())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
println!("device: {}, {:?}", device_id, result2);
|
||||||
|
assert_eq!(result2.network_config, network_config);
|
||||||
|
|
||||||
|
assert_eq!(result.create_time, result2.create_time);
|
||||||
|
assert_ne!(result.update_time, result2.update_time);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
db.list_network_configs(user_id, Some(device_id), true)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.len(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
db.delete_network_config(user_id, inst_id).await.unwrap();
|
||||||
|
let result3 = user_running_network_configs::Entity::find()
|
||||||
|
.filter(user_running_network_configs::Column::UserId.eq(user_id))
|
||||||
|
.one(db.orm_db())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(result3.is_none());
|
||||||
|
}
|
||||||
|
}
|
38
easytier-web/src/main.rs
Normal file
38
easytier-web/src/main.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use easytier::{
|
||||||
|
common::config::{ConfigLoader, ConsoleLoggerConfig, TomlConfigLoader},
|
||||||
|
tunnel::udp::UdpTunnelListener,
|
||||||
|
utils::init_logger,
|
||||||
|
};
|
||||||
|
|
||||||
|
mod client_manager;
|
||||||
|
mod db;
|
||||||
|
mod migrator;
|
||||||
|
mod restful;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let config = TomlConfigLoader::default();
|
||||||
|
config.set_console_logger_config(ConsoleLoggerConfig {
|
||||||
|
level: Some("trace".to_string()),
|
||||||
|
});
|
||||||
|
init_logger(config, false).unwrap();
|
||||||
|
|
||||||
|
// let db = db::Db::new(":memory:").await.unwrap();
|
||||||
|
let db = db::Db::new("et.db").await.unwrap();
|
||||||
|
|
||||||
|
let listener = UdpTunnelListener::new("udp://0.0.0.0:22020".parse().unwrap());
|
||||||
|
let mut mgr = client_manager::ClientManager::new(db.clone());
|
||||||
|
mgr.serve(listener).await.unwrap();
|
||||||
|
let mgr = Arc::new(mgr);
|
||||||
|
|
||||||
|
let mut restful_server =
|
||||||
|
restful::RestfulServer::new("0.0.0.0:11211".parse().unwrap(), mgr.clone(), db)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
restful_server.start().await.unwrap();
|
||||||
|
tokio::signal::ctrl_c().await.unwrap();
|
||||||
|
}
|
364
easytier-web/src/migrator/m20241029_000001_init.rs
Normal file
364
easytier-web/src/migrator/m20241029_000001_init.rs
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
// src/migrator/m20220602_000001_create_bakery_table.rs (create new file)
|
||||||
|
|
||||||
|
use sea_orm_migration::{prelude::*, schema::*};
|
||||||
|
|
||||||
|
pub struct Migration;
|
||||||
|
|
||||||
|
impl MigrationName for Migration {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"m20241029_000001_init"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(DeriveIden)]
|
||||||
|
pub enum Users {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
Username,
|
||||||
|
Password,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(DeriveIden)]
|
||||||
|
enum Groups {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(DeriveIden)]
|
||||||
|
enum Permissions {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(DeriveIden)]
|
||||||
|
enum UsersGroups {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
UserId,
|
||||||
|
GroupId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(DeriveIden)]
|
||||||
|
enum GroupsPermissions {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
GroupId,
|
||||||
|
PermissionId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(DeriveIden)]
|
||||||
|
enum UserRunningNetworkConfigs {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
UserId,
|
||||||
|
DeviceId,
|
||||||
|
NetworkInstanceId,
|
||||||
|
NetworkConfig,
|
||||||
|
Disabled,
|
||||||
|
CreateTime,
|
||||||
|
UpdateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
// Define how to apply this migration: Create the Bakery table.
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
// Create the `users` table.
|
||||||
|
manager
|
||||||
|
.create_table(
|
||||||
|
Table::create()
|
||||||
|
.if_not_exists()
|
||||||
|
.table(Users::Table)
|
||||||
|
.col(pk_auto(Users::Id).not_null())
|
||||||
|
.col(string(Users::Username).not_null().unique_key())
|
||||||
|
.col(string(Users::Password).not_null())
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
manager
|
||||||
|
.create_index(
|
||||||
|
Index::create()
|
||||||
|
.name("idx_users_username")
|
||||||
|
.table(Users::Table)
|
||||||
|
.col(Users::Username)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Create the `groups` table.
|
||||||
|
manager
|
||||||
|
.create_table(
|
||||||
|
Table::create()
|
||||||
|
.if_not_exists()
|
||||||
|
.table(Groups::Table)
|
||||||
|
.col(pk_auto(Groups::Id).not_null())
|
||||||
|
.col(string(Groups::Name).not_null().unique_key())
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
manager
|
||||||
|
.create_index(
|
||||||
|
Index::create()
|
||||||
|
.name("idx_groups_name")
|
||||||
|
.table(Groups::Table)
|
||||||
|
.col(Groups::Name)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Create the `permissions` table.
|
||||||
|
manager
|
||||||
|
.create_table(
|
||||||
|
Table::create()
|
||||||
|
.if_not_exists()
|
||||||
|
.table(Permissions::Table)
|
||||||
|
.col(pk_auto(Permissions::Id).not_null())
|
||||||
|
.col(string(Permissions::Name).not_null().unique_key())
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Create the `users_groups` table.
|
||||||
|
manager
|
||||||
|
.create_table(
|
||||||
|
Table::create()
|
||||||
|
.if_not_exists()
|
||||||
|
.table(UsersGroups::Table)
|
||||||
|
.col(pk_auto(UsersGroups::Id).not_null())
|
||||||
|
.col(integer(UsersGroups::UserId).not_null())
|
||||||
|
.col(integer(UsersGroups::GroupId).not_null())
|
||||||
|
.foreign_key(
|
||||||
|
ForeignKey::create()
|
||||||
|
.name("fk_users_groups_user_id_to_users_id")
|
||||||
|
.from(UsersGroups::Table, UsersGroups::UserId)
|
||||||
|
.to(Users::Table, Users::Id)
|
||||||
|
.on_delete(ForeignKeyAction::Cascade)
|
||||||
|
.on_update(ForeignKeyAction::Cascade),
|
||||||
|
)
|
||||||
|
.foreign_key(
|
||||||
|
ForeignKey::create()
|
||||||
|
.name("fk_users_groups_group_id_to_groups_id")
|
||||||
|
.from(UsersGroups::Table, UsersGroups::GroupId)
|
||||||
|
.to(Groups::Table, Groups::Id)
|
||||||
|
.on_delete(ForeignKeyAction::Cascade)
|
||||||
|
.on_update(ForeignKeyAction::Cascade),
|
||||||
|
)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Create the `groups_permissions` table.
|
||||||
|
manager
|
||||||
|
.create_table(
|
||||||
|
Table::create()
|
||||||
|
.if_not_exists()
|
||||||
|
.table(GroupsPermissions::Table)
|
||||||
|
.col(pk_auto(GroupsPermissions::Id).not_null())
|
||||||
|
.col(integer(GroupsPermissions::GroupId).not_null())
|
||||||
|
.col(integer(GroupsPermissions::PermissionId).not_null())
|
||||||
|
.foreign_key(
|
||||||
|
ForeignKey::create()
|
||||||
|
.name("fk_groups_permissions_group_id_to_groups_id")
|
||||||
|
.from(GroupsPermissions::Table, GroupsPermissions::GroupId)
|
||||||
|
.to(Groups::Table, Groups::Id)
|
||||||
|
.on_delete(ForeignKeyAction::Cascade)
|
||||||
|
.on_update(ForeignKeyAction::Cascade),
|
||||||
|
)
|
||||||
|
.foreign_key(
|
||||||
|
ForeignKey::create()
|
||||||
|
.name("fk_groups_permissions_permission_id_to_permissions_id")
|
||||||
|
.from(GroupsPermissions::Table, GroupsPermissions::PermissionId)
|
||||||
|
.to(Permissions::Table, Permissions::Id)
|
||||||
|
.on_delete(ForeignKeyAction::Cascade)
|
||||||
|
.on_update(ForeignKeyAction::Cascade),
|
||||||
|
)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// create user running network configs table
|
||||||
|
manager
|
||||||
|
.create_table(
|
||||||
|
Table::create()
|
||||||
|
.if_not_exists()
|
||||||
|
.table(UserRunningNetworkConfigs::Table)
|
||||||
|
.col(pk_auto(UserRunningNetworkConfigs::Id).not_null())
|
||||||
|
.col(integer(UserRunningNetworkConfigs::UserId).not_null())
|
||||||
|
.col(text(UserRunningNetworkConfigs::DeviceId).not_null())
|
||||||
|
.col(
|
||||||
|
text(UserRunningNetworkConfigs::NetworkInstanceId)
|
||||||
|
.unique_key()
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.col(text(UserRunningNetworkConfigs::NetworkConfig).not_null())
|
||||||
|
.col(
|
||||||
|
boolean(UserRunningNetworkConfigs::Disabled)
|
||||||
|
.not_null()
|
||||||
|
.default(false),
|
||||||
|
)
|
||||||
|
.col(timestamp_with_time_zone(UserRunningNetworkConfigs::CreateTime).not_null())
|
||||||
|
.col(timestamp_with_time_zone(UserRunningNetworkConfigs::UpdateTime).not_null())
|
||||||
|
.foreign_key(
|
||||||
|
ForeignKey::create()
|
||||||
|
.name("fk_user_running_network_configs_user_id_to_users_id")
|
||||||
|
.from(
|
||||||
|
UserRunningNetworkConfigs::Table,
|
||||||
|
UserRunningNetworkConfigs::UserId,
|
||||||
|
)
|
||||||
|
.to(Users::Table, Users::Id)
|
||||||
|
.on_delete(ForeignKeyAction::Cascade)
|
||||||
|
.on_update(ForeignKeyAction::Cascade),
|
||||||
|
)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
manager
|
||||||
|
.create_index(
|
||||||
|
Index::create()
|
||||||
|
.name("idx_user_running_network_configs_user_id")
|
||||||
|
.table(UserRunningNetworkConfigs::Table)
|
||||||
|
.col(UserRunningNetworkConfigs::UserId)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// prepare data
|
||||||
|
let user = Query::insert()
|
||||||
|
.into_table(Users::Table)
|
||||||
|
.columns(vec![Users::Username, Users::Password])
|
||||||
|
.values_panic(vec![
|
||||||
|
"user".into(),
|
||||||
|
"$argon2i$v=19$m=16,t=2,p=1$aGVyRDBrcnRycnlaMDhkbw$449SEcv/qXf+0fnI9+fYVQ".into(), // user (md5summed)
|
||||||
|
])
|
||||||
|
.to_owned();
|
||||||
|
manager.exec_stmt(user).await?;
|
||||||
|
|
||||||
|
let admin = Query::insert()
|
||||||
|
.into_table(Users::Table)
|
||||||
|
.columns(vec![Users::Username, Users::Password])
|
||||||
|
.values_panic(vec![
|
||||||
|
"admin".into(),
|
||||||
|
"$argon2i$v=19$m=16,t=2,p=1$bW5idXl0cmY$61n+JxL4r3dwLPAEDlDdtg".into(), // admin (md5summed)
|
||||||
|
])
|
||||||
|
.to_owned();
|
||||||
|
manager.exec_stmt(admin).await?;
|
||||||
|
|
||||||
|
let users = Query::insert()
|
||||||
|
.into_table(Groups::Table)
|
||||||
|
.columns(vec![Groups::Name])
|
||||||
|
.values_panic(vec!["users".into()])
|
||||||
|
.to_owned();
|
||||||
|
manager.exec_stmt(users).await?;
|
||||||
|
|
||||||
|
let admins = Query::insert()
|
||||||
|
.into_table(Groups::Table)
|
||||||
|
.columns(vec![Groups::Name])
|
||||||
|
.values_panic(vec!["admins".into()])
|
||||||
|
.to_owned();
|
||||||
|
manager.exec_stmt(admins).await?;
|
||||||
|
|
||||||
|
let sessions = Query::insert()
|
||||||
|
.into_table(Permissions::Table)
|
||||||
|
.columns(vec![Permissions::Name])
|
||||||
|
.values_panic(vec!["sessions".into()])
|
||||||
|
.to_owned();
|
||||||
|
manager.exec_stmt(sessions).await?;
|
||||||
|
|
||||||
|
let devices = Query::insert()
|
||||||
|
.into_table(Permissions::Table)
|
||||||
|
.columns(vec![Permissions::Name])
|
||||||
|
.values_panic(vec!["devices".into()])
|
||||||
|
.to_owned();
|
||||||
|
manager.exec_stmt(devices).await?;
|
||||||
|
|
||||||
|
let users_devices = Query::insert()
|
||||||
|
.into_table(GroupsPermissions::Table)
|
||||||
|
.columns(vec![
|
||||||
|
GroupsPermissions::GroupId,
|
||||||
|
GroupsPermissions::PermissionId,
|
||||||
|
])
|
||||||
|
.select_from(
|
||||||
|
Query::select()
|
||||||
|
.column((Groups::Table, Groups::Id))
|
||||||
|
.column((Permissions::Table, Permissions::Id))
|
||||||
|
.from(Groups::Table)
|
||||||
|
.full_outer_join(Permissions::Table, all![])
|
||||||
|
.cond_where(any![
|
||||||
|
// users have devices permission
|
||||||
|
Expr::col((Groups::Table, Groups::Name))
|
||||||
|
.eq("users")
|
||||||
|
.and(Expr::col((Permissions::Table, Permissions::Name)).eq("devices")),
|
||||||
|
// admins have all permissions
|
||||||
|
Expr::col((Groups::Table, Groups::Name)).eq("admins"),
|
||||||
|
])
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.to_owned();
|
||||||
|
manager.exec_stmt(users_devices).await?;
|
||||||
|
|
||||||
|
let add_user_to_users = Query::insert()
|
||||||
|
.into_table(UsersGroups::Table)
|
||||||
|
.columns(vec![UsersGroups::UserId, UsersGroups::GroupId])
|
||||||
|
.select_from(
|
||||||
|
Query::select()
|
||||||
|
.column((Users::Table, Users::Id))
|
||||||
|
.column((Groups::Table, Groups::Id))
|
||||||
|
.from(Users::Table)
|
||||||
|
.full_outer_join(Groups::Table, all![])
|
||||||
|
.cond_where(
|
||||||
|
Expr::col(Users::Username)
|
||||||
|
.eq("user")
|
||||||
|
.and(Expr::col(Groups::Name).eq("users")),
|
||||||
|
)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.to_owned();
|
||||||
|
manager.exec_stmt(add_user_to_users).await?;
|
||||||
|
|
||||||
|
let add_admin_to_admins = Query::insert()
|
||||||
|
.into_table(UsersGroups::Table)
|
||||||
|
.columns(vec![UsersGroups::UserId, UsersGroups::GroupId])
|
||||||
|
.select_from(
|
||||||
|
Query::select()
|
||||||
|
.column((Users::Table, Users::Id))
|
||||||
|
.column((Groups::Table, Groups::Id))
|
||||||
|
.from(Users::Table)
|
||||||
|
.full_outer_join(Groups::Table, all![])
|
||||||
|
.cond_where(
|
||||||
|
Expr::col(Users::Username)
|
||||||
|
.eq("admin")
|
||||||
|
.and(Expr::col(Groups::Name).eq("admins")),
|
||||||
|
)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.to_owned();
|
||||||
|
manager.exec_stmt(add_admin_to_admins).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define how to rollback this migration: Drop the Bakery table.
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.drop_table(Table::drop().table(Users::Table).to_owned())
|
||||||
|
.await?;
|
||||||
|
manager
|
||||||
|
.drop_table(Table::drop().table(Groups::Table).to_owned())
|
||||||
|
.await?;
|
||||||
|
manager
|
||||||
|
.drop_table(Table::drop().table(Permissions::Table).to_owned())
|
||||||
|
.await?;
|
||||||
|
manager
|
||||||
|
.drop_table(Table::drop().table(UsersGroups::Table).to_owned())
|
||||||
|
.await?;
|
||||||
|
manager
|
||||||
|
.drop_table(Table::drop().table(GroupsPermissions::Table).to_owned())
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user