mirror of
				https://github.com/EasyTier/EasyTier.git
				synced 2025-10-31 12:06:28 +08:00 
			
		
		
		
	Compare commits
	
		
			36 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 34e4e907a9 | ||
|   | 2f4a097787 | ||
|   | f3de00be37 | ||
|   | 4cf61f0d4a | ||
|   | 4e5915f98e | ||
|   | 870eca9e9f | ||
|   | 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 | 
							
								
								
									
										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.3' |         default: 'v2.1.1' | ||||||
|         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 | ||||||
|   | |||||||
							
								
								
									
										2471
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2471
									
								
								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" | ||||||
|   | |||||||
| @@ -14,6 +14,10 @@ | |||||||
| 		{ | 		{ | ||||||
| 			"name": "vpnservice", | 			"name": "vpnservice", | ||||||
| 			"path": "tauri-plugin-vpnservice" | 			"path": "tauri-plugin-vpnservice" | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"name": "rpc-build", | ||||||
|  | 			"path": "easytier-rpc-build" | ||||||
| 		} | 		} | ||||||
| 	], | 	], | ||||||
| 	"settings": { | 	"settings": { | ||||||
|   | |||||||
| @@ -1,2 +0,0 @@ | |||||||
| shamefully-hoist=true |  | ||||||
| strict-peer-dependencies=false |  | ||||||
| @@ -1,8 +1,9 @@ | |||||||
| { | { | ||||||
|   "name": "easytier-gui", |   "name": "easytier-gui", | ||||||
|   "type": "module", |   "type": "module", | ||||||
|   "version": "2.0.3", |   "version": "2.1.1", | ||||||
|   "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,44 +13,44 @@ | |||||||
|     "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.1.0", |     "@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/default-gateway": "^7.2.2", | ||||||
|     "@types/node": "^22.7.4", |     "@types/node": "^22.7.4", | ||||||
|     "@types/uuid": "^10.0.0", |     "@types/uuid": "^10.0.0", | ||||||
|     "@vitejs/plugin-vue": "^5.1.4", |     "@vitejs/plugin-vue": "^5.1.4", | ||||||
|     "@vue-macros/volar": "0.30.3", |     "@vue-macros/volar": "0.30.5", | ||||||
|     "autoprefixer": "^10.4.20", |     "autoprefixer": "^10.4.20", | ||||||
|  |     "cidr-tools": "^11.0.2", | ||||||
|  |     "default-gateway": "^7.2.2", | ||||||
|     "eslint": "^9.12.0", |     "eslint": "^9.12.0", | ||||||
|     "eslint-plugin-format": "^0.1.2", |     "eslint-plugin-format": "^0.1.2", | ||||||
|     "internal-ip": "^8.0.0", |  | ||||||
|     "postcss": "^8.4.47", |     "postcss": "^8.4.47", | ||||||
|     "tailwindcss": "^3.4.13", |     "tailwindcss": "^3.4.13", | ||||||
|     "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", | ||||||
| @@ -57,7 +58,6 @@ | |||||||
|     "vite-plugin-vue-devtools": "^7.4.6", |     "vite-plugin-vue-devtools": "^7.4.6", | ||||||
|     "vite-plugin-vue-layouts": "^0.11.0", |     "vite-plugin-vue-layouts": "^0.11.0", | ||||||
|     "vue-i18n": "^10.0.0", |     "vue-i18n": "^10.0.0", | ||||||
|     "vue-tsc": "^2.1.6" |     "vue-tsc": "^2.1.10" | ||||||
|   }, |   } | ||||||
|   "packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4" |  | ||||||
| } | } | ||||||
							
								
								
									
										446
									
								
								easytier-gui/pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										446
									
								
								easytier-gui/pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							| @@ -28,7 +28,7 @@ importers: | |||||||
|         version: 2.0.0-rc.1 |         version: 2.0.0-rc.1 | ||||||
|       '@vueuse/core': |       '@vueuse/core': | ||||||
|         specifier: ^11.1.0 |         specifier: ^11.1.0 | ||||||
|         version: 11.1.0(vue@3.5.11(typescript@5.6.3)) |         version: 11.1.0(vue@3.4.38(typescript@5.6.3)) | ||||||
|       aura: |       aura: | ||||||
|         specifier: link:@primevue\themes\aura |         specifier: link:@primevue\themes\aura | ||||||
|         version: link:@primevue/themes/aura |         version: link:@primevue/themes/aura | ||||||
| @@ -37,7 +37,7 @@ importers: | |||||||
|         version: 1.5.1 |         version: 1.5.1 | ||||||
|       pinia: |       pinia: | ||||||
|         specifier: ^2.2.4 |         specifier: ^2.2.4 | ||||||
|         version: 2.2.4(typescript@5.6.3)(vue@3.5.11(typescript@5.6.3)) |         version: 2.2.4(typescript@5.6.3)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       primeflex: |       primeflex: | ||||||
|         specifier: ^3.3.1 |         specifier: ^3.3.1 | ||||||
|         version: 3.3.1 |         version: 3.3.1 | ||||||
| @@ -46,26 +46,26 @@ importers: | |||||||
|         version: 7.0.0 |         version: 7.0.0 | ||||||
|       primevue: |       primevue: | ||||||
|         specifier: ^4.1.0 |         specifier: ^4.1.0 | ||||||
|         version: 4.1.0(vue@3.5.11(typescript@5.6.3)) |         version: 4.1.0(vue@3.4.38(typescript@5.6.3)) | ||||||
|       tauri-plugin-vpnservice-api: |       tauri-plugin-vpnservice-api: | ||||||
|         specifier: link:..\tauri-plugin-vpnservice |         specifier: link:..\tauri-plugin-vpnservice | ||||||
|         version: link:../tauri-plugin-vpnservice |         version: link:../tauri-plugin-vpnservice | ||||||
|       vue: |       vue: | ||||||
|         specifier: ^3.5.11 |         specifier: '=3.4.38' | ||||||
|         version: 3.5.11(typescript@5.6.3) |         version: 3.4.38(typescript@5.6.3) | ||||||
|       vue-i18n: |       vue-i18n: | ||||||
|         specifier: ^10.0.4 |         specifier: ^10.0.4 | ||||||
|         version: 10.0.4(vue@3.5.11(typescript@5.6.3)) |         version: 10.0.4(vue@3.4.38(typescript@5.6.3)) | ||||||
|       vue-router: |       vue-router: | ||||||
|         specifier: ^4.4.5 |         specifier: ^4.4.5 | ||||||
|         version: 4.4.5(vue@3.5.11(typescript@5.6.3)) |         version: 4.4.5(vue@3.4.38(typescript@5.6.3)) | ||||||
|     devDependencies: |     devDependencies: | ||||||
|       '@antfu/eslint-config': |       '@antfu/eslint-config': | ||||||
|         specifier: ^3.7.3 |         specifier: ^3.7.3 | ||||||
|         version: 3.7.3(@typescript-eslint/utils@8.8.1(eslint@9.12.0(jiti@1.21.6))(typescript@5.6.3))(@vue/compiler-sfc@3.5.11)(eslint-plugin-format@0.1.2(eslint@9.12.0(jiti@1.21.6)))(eslint@9.12.0(jiti@1.21.6))(typescript@5.6.3) |         version: 3.7.3(@typescript-eslint/utils@8.8.1(eslint@9.12.0(jiti@1.21.6))(typescript@5.6.3))(@vue/compiler-sfc@3.5.11)(eslint-plugin-format@0.1.2(eslint@9.12.0(jiti@1.21.6)))(eslint@9.12.0(jiti@1.21.6))(typescript@5.6.3) | ||||||
|       '@intlify/unplugin-vue-i18n': |       '@intlify/unplugin-vue-i18n': | ||||||
|         specifier: ^5.2.0 |         specifier: ^5.2.0 | ||||||
|         version: 5.2.0(@vue/compiler-dom@3.5.11)(eslint@9.12.0(jiti@1.21.6))(rollup@4.24.0)(typescript@5.6.3)(vue-i18n@10.0.4(vue@3.5.11(typescript@5.6.3)))(vue@3.5.11(typescript@5.6.3)) |         version: 5.2.0(@vue/compiler-dom@3.5.11)(eslint@9.12.0(jiti@1.21.6))(rollup@4.24.0)(typescript@5.6.3)(vue-i18n@10.0.4(vue@3.4.38(typescript@5.6.3)))(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@primevue/auto-import-resolver': |       '@primevue/auto-import-resolver': | ||||||
|         specifier: ^4.1.0 |         specifier: ^4.1.0 | ||||||
|         version: 4.1.0 |         version: 4.1.0 | ||||||
| @@ -83,10 +83,10 @@ importers: | |||||||
|         version: 10.0.0 |         version: 10.0.0 | ||||||
|       '@vitejs/plugin-vue': |       '@vitejs/plugin-vue': | ||||||
|         specifier: ^5.1.4 |         specifier: ^5.1.4 | ||||||
|         version: 5.1.4(vite@5.4.8(@types/node@22.7.5))(vue@3.5.11(typescript@5.6.3)) |         version: 5.1.4(vite@5.4.8(@types/node@22.7.5))(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/volar': |       '@vue-macros/volar': | ||||||
|         specifier: 0.30.3 |         specifier: 0.30.3 | ||||||
|         version: 0.30.3(rollup@4.24.0)(typescript@5.6.3)(vue-tsc@2.1.6(typescript@5.6.3))(vue@3.5.11(typescript@5.6.3)) |         version: 0.30.3(rollup@4.24.0)(typescript@5.6.3)(vue-tsc@2.1.6(typescript@5.6.3))(vue@3.4.38(typescript@5.6.3)) | ||||||
|       autoprefixer: |       autoprefixer: | ||||||
|         specifier: ^10.4.20 |         specifier: ^10.4.20 | ||||||
|         version: 10.4.20(postcss@8.4.47) |         version: 10.4.20(postcss@8.4.47) | ||||||
| @@ -110,19 +110,19 @@ importers: | |||||||
|         version: 5.6.3 |         version: 5.6.3 | ||||||
|       unplugin-auto-import: |       unplugin-auto-import: | ||||||
|         specifier: ^0.18.3 |         specifier: ^0.18.3 | ||||||
|         version: 0.18.3(@vueuse/core@11.1.0(vue@3.5.11(typescript@5.6.3)))(rollup@4.24.0) |         version: 0.18.3(@vueuse/core@11.1.0(vue@3.4.38(typescript@5.6.3)))(rollup@4.24.0) | ||||||
|       unplugin-vue-components: |       unplugin-vue-components: | ||||||
|         specifier: ^0.27.4 |         specifier: ^0.27.4 | ||||||
|         version: 0.27.4(@babel/parser@7.25.8)(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |         version: 0.27.4(@babel/parser@7.25.8)(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       unplugin-vue-macros: |       unplugin-vue-macros: | ||||||
|         specifier: ^2.12.3 |         specifier: ^2.12.3 | ||||||
|         version: 2.12.3(@vueuse/core@11.1.0(vue@3.5.11(typescript@5.6.3)))(esbuild@0.23.1)(rollup@4.24.0)(typescript@5.6.3)(vite@5.4.8(@types/node@22.7.5))(vue-tsc@2.1.6(typescript@5.6.3))(vue@3.5.11(typescript@5.6.3)) |         version: 2.12.3(@vueuse/core@11.1.0(vue@3.4.38(typescript@5.6.3)))(esbuild@0.23.1)(rollup@4.24.0)(typescript@5.6.3)(vite@5.4.8(@types/node@22.7.5))(vue-tsc@2.1.6(typescript@5.6.3))(vue@3.4.38(typescript@5.6.3)) | ||||||
|       unplugin-vue-markdown: |       unplugin-vue-markdown: | ||||||
|         specifier: ^0.26.2 |         specifier: ^0.26.2 | ||||||
|         version: 0.26.2(rollup@4.24.0)(vite@5.4.8(@types/node@22.7.5)) |         version: 0.26.2(rollup@4.24.0)(vite@5.4.8(@types/node@22.7.5)) | ||||||
|       unplugin-vue-router: |       unplugin-vue-router: | ||||||
|         specifier: ^0.10.8 |         specifier: ^0.10.8 | ||||||
|         version: 0.10.8(rollup@4.24.0)(vue-router@4.4.5(vue@3.5.11(typescript@5.6.3)))(vue@3.5.11(typescript@5.6.3)) |         version: 0.10.8(rollup@4.24.0)(vue-router@4.4.5(vue@3.4.38(typescript@5.6.3)))(vue@3.4.38(typescript@5.6.3)) | ||||||
|       uuid: |       uuid: | ||||||
|         specifier: ^10.0.0 |         specifier: ^10.0.0 | ||||||
|         version: 10.0.0 |         version: 10.0.0 | ||||||
| @@ -131,10 +131,10 @@ importers: | |||||||
|         version: 5.4.8(@types/node@22.7.5) |         version: 5.4.8(@types/node@22.7.5) | ||||||
|       vite-plugin-vue-devtools: |       vite-plugin-vue-devtools: | ||||||
|         specifier: ^7.4.6 |         specifier: ^7.4.6 | ||||||
|         version: 7.4.6(rollup@4.24.0)(vite@5.4.8(@types/node@22.7.5))(vue@3.5.11(typescript@5.6.3)) |         version: 7.4.6(rollup@4.24.0)(vite@5.4.8(@types/node@22.7.5))(vue@3.4.38(typescript@5.6.3)) | ||||||
|       vite-plugin-vue-layouts: |       vite-plugin-vue-layouts: | ||||||
|         specifier: ^0.11.0 |         specifier: ^0.11.0 | ||||||
|         version: 0.11.0(vite@5.4.8(@types/node@22.7.5))(vue-router@4.4.5(vue@3.5.11(typescript@5.6.3)))(vue@3.5.11(typescript@5.6.3)) |         version: 0.11.0(vite@5.4.8(@types/node@22.7.5))(vue-router@4.4.5(vue@3.4.38(typescript@5.6.3)))(vue@3.4.38(typescript@5.6.3)) | ||||||
|       vue-tsc: |       vue-tsc: | ||||||
|         specifier: ^2.1.6 |         specifier: ^2.1.6 | ||||||
|         version: 2.1.6(typescript@5.6.3) |         version: 2.1.6(typescript@5.6.3) | ||||||
| @@ -1380,15 +1380,27 @@ packages: | |||||||
|     peerDependencies: |     peerDependencies: | ||||||
|       '@babel/core': ^7.0.0-0 |       '@babel/core': ^7.0.0-0 | ||||||
|  |  | ||||||
|  |   '@vue/compiler-core@3.4.38': | ||||||
|  |     resolution: {integrity: sha512-8IQOTCWnLFqfHzOGm9+P8OPSEDukgg3Huc92qSG49if/xI2SAwLHQO2qaPQbjCWPBcQoO1WYfXfTACUrWV3c5A==} | ||||||
|  |  | ||||||
|   '@vue/compiler-core@3.5.11': |   '@vue/compiler-core@3.5.11': | ||||||
|     resolution: {integrity: sha512-PwAdxs7/9Hc3ieBO12tXzmTD+Ln4qhT/56S+8DvrrZ4kLDn4Z/AMUr8tXJD0axiJBS0RKIoNaR0yMuQB9v9Udg==} |     resolution: {integrity: sha512-PwAdxs7/9Hc3ieBO12tXzmTD+Ln4qhT/56S+8DvrrZ4kLDn4Z/AMUr8tXJD0axiJBS0RKIoNaR0yMuQB9v9Udg==} | ||||||
|  |  | ||||||
|  |   '@vue/compiler-dom@3.4.38': | ||||||
|  |     resolution: {integrity: sha512-Osc/c7ABsHXTsETLgykcOwIxFktHfGSUDkb05V61rocEfsFDcjDLH/IHJSNJP+/Sv9KeN2Lx1V6McZzlSb9EhQ==} | ||||||
|  |  | ||||||
|   '@vue/compiler-dom@3.5.11': |   '@vue/compiler-dom@3.5.11': | ||||||
|     resolution: {integrity: sha512-pyGf8zdbDDRkBrEzf8p7BQlMKNNF5Fk/Cf/fQ6PiUz9at4OaUfyXW0dGJTo2Vl1f5U9jSLCNf0EZJEogLXoeew==} |     resolution: {integrity: sha512-pyGf8zdbDDRkBrEzf8p7BQlMKNNF5Fk/Cf/fQ6PiUz9at4OaUfyXW0dGJTo2Vl1f5U9jSLCNf0EZJEogLXoeew==} | ||||||
|  |  | ||||||
|  |   '@vue/compiler-sfc@3.4.38': | ||||||
|  |     resolution: {integrity: sha512-s5QfZ+9PzPh3T5H4hsQDJtI8x7zdJaew/dCGgqZ2630XdzaZ3AD8xGZfBqpT8oaD/p2eedd+pL8tD5vvt5ZYJQ==} | ||||||
|  |  | ||||||
|   '@vue/compiler-sfc@3.5.11': |   '@vue/compiler-sfc@3.5.11': | ||||||
|     resolution: {integrity: sha512-gsbBtT4N9ANXXepprle+X9YLg2htQk1sqH/qGJ/EApl+dgpUBdTv3yP7YlR535uHZY3n6XaR0/bKo0BgwwDniw==} |     resolution: {integrity: sha512-gsbBtT4N9ANXXepprle+X9YLg2htQk1sqH/qGJ/EApl+dgpUBdTv3yP7YlR535uHZY3n6XaR0/bKo0BgwwDniw==} | ||||||
|  |  | ||||||
|  |   '@vue/compiler-ssr@3.4.38': | ||||||
|  |     resolution: {integrity: sha512-YXznKFQ8dxYpAz9zLuVvfcXhc31FSPFDcqr0kyujbOwNhlmaNvL2QfIy+RZeJgSn5Fk54CWoEUeW+NVBAogGaw==} | ||||||
|  |  | ||||||
|   '@vue/compiler-ssr@3.5.11': |   '@vue/compiler-ssr@3.5.11': | ||||||
|     resolution: {integrity: sha512-P4+GPjOuC2aFTk1Z4WANvEhyOykcvEd5bIj2KVNGKGfM745LaXGr++5njpdBTzVz5pZifdlR1kpYSJJpIlSePA==} |     resolution: {integrity: sha512-P4+GPjOuC2aFTk1Z4WANvEhyOykcvEd5bIj2KVNGKGfM745LaXGr++5njpdBTzVz5pZifdlR1kpYSJJpIlSePA==} | ||||||
|  |  | ||||||
| @@ -1417,20 +1429,37 @@ packages: | |||||||
|       typescript: |       typescript: | ||||||
|         optional: true |         optional: true | ||||||
|  |  | ||||||
|  |   '@vue/reactivity@3.4.38': | ||||||
|  |     resolution: {integrity: sha512-4vl4wMMVniLsSYYeldAKzbk72+D3hUnkw9z8lDeJacTxAkXeDAP1uE9xr2+aKIN0ipOL8EG2GPouVTH6yF7Gnw==} | ||||||
|  |  | ||||||
|   '@vue/reactivity@3.5.11': |   '@vue/reactivity@3.5.11': | ||||||
|     resolution: {integrity: sha512-Nqo5VZEn8MJWlCce8XoyVqHZbd5P2NH+yuAaFzuNSR96I+y1cnuUiq7xfSG+kyvLSiWmaHTKP1r3OZY4mMD50w==} |     resolution: {integrity: sha512-Nqo5VZEn8MJWlCce8XoyVqHZbd5P2NH+yuAaFzuNSR96I+y1cnuUiq7xfSG+kyvLSiWmaHTKP1r3OZY4mMD50w==} | ||||||
|  |  | ||||||
|  |   '@vue/runtime-core@3.4.38': | ||||||
|  |     resolution: {integrity: sha512-21z3wA99EABtuf+O3IhdxP0iHgkBs1vuoCAsCKLVJPEjpVqvblwBnTj42vzHRlWDCyxu9ptDm7sI2ZMcWrQqlA==} | ||||||
|  |  | ||||||
|   '@vue/runtime-core@3.5.11': |   '@vue/runtime-core@3.5.11': | ||||||
|     resolution: {integrity: sha512-7PsxFGqwfDhfhh0OcDWBG1DaIQIVOLgkwA5q6MtkPiDFjp5gohVnJEahSktwSFLq7R5PtxDKy6WKURVN1UDbzA==} |     resolution: {integrity: sha512-7PsxFGqwfDhfhh0OcDWBG1DaIQIVOLgkwA5q6MtkPiDFjp5gohVnJEahSktwSFLq7R5PtxDKy6WKURVN1UDbzA==} | ||||||
|  |  | ||||||
|  |   '@vue/runtime-dom@3.4.38': | ||||||
|  |     resolution: {integrity: sha512-afZzmUreU7vKwKsV17H1NDThEEmdYI+GCAK/KY1U957Ig2NATPVjCROv61R19fjZNzMmiU03n79OMnXyJVN0UA==} | ||||||
|  |  | ||||||
|   '@vue/runtime-dom@3.5.11': |   '@vue/runtime-dom@3.5.11': | ||||||
|     resolution: {integrity: sha512-GNghjecT6IrGf0UhuYmpgaOlN7kxzQBhxWEn08c/SQDxv1yy4IXI1bn81JgEpQ4IXjRxWtPyI8x0/7TF5rPfYQ==} |     resolution: {integrity: sha512-GNghjecT6IrGf0UhuYmpgaOlN7kxzQBhxWEn08c/SQDxv1yy4IXI1bn81JgEpQ4IXjRxWtPyI8x0/7TF5rPfYQ==} | ||||||
|  |  | ||||||
|  |   '@vue/server-renderer@3.4.38': | ||||||
|  |     resolution: {integrity: sha512-NggOTr82FbPEkkUvBm4fTGcwUY8UuTsnWC/L2YZBmvaQ4C4Jl/Ao4HHTB+l7WnFCt5M/dN3l0XLuyjzswGYVCA==} | ||||||
|  |     peerDependencies: | ||||||
|  |       vue: 3.4.38 | ||||||
|  |  | ||||||
|   '@vue/server-renderer@3.5.11': |   '@vue/server-renderer@3.5.11': | ||||||
|     resolution: {integrity: sha512-cVOwYBxR7Wb1B1FoxYvtjJD8X/9E5nlH4VSkJy2uMA1MzYNdzAAB//l8nrmN9py/4aP+3NjWukf9PZ3TeWULaA==} |     resolution: {integrity: sha512-cVOwYBxR7Wb1B1FoxYvtjJD8X/9E5nlH4VSkJy2uMA1MzYNdzAAB//l8nrmN9py/4aP+3NjWukf9PZ3TeWULaA==} | ||||||
|     peerDependencies: |     peerDependencies: | ||||||
|       vue: 3.5.11 |       vue: 3.5.11 | ||||||
|  |  | ||||||
|  |   '@vue/shared@3.4.38': | ||||||
|  |     resolution: {integrity: sha512-q0xCiLkuWWQLzVrecPb0RMsNWyxICOjPrcrwxTUEHb1fsnvni4dcuyG7RT/Ie7VPTvnjzIaWzRMUBsrqNj/hhw==} | ||||||
|  |  | ||||||
|   '@vue/shared@3.5.11': |   '@vue/shared@3.5.11': | ||||||
|     resolution: {integrity: sha512-W8GgysJVnFo81FthhzurdRAWP/byq3q2qIw70e0JWblzVhjgOMiC2GyovXrZTFQJnFVryYaKGP3Tc9vYzYm6PQ==} |     resolution: {integrity: sha512-W8GgysJVnFo81FthhzurdRAWP/byq3q2qIw70e0JWblzVhjgOMiC2GyovXrZTFQJnFVryYaKGP3Tc9vYzYm6PQ==} | ||||||
|  |  | ||||||
| @@ -3369,6 +3398,14 @@ packages: | |||||||
|     peerDependencies: |     peerDependencies: | ||||||
|       typescript: '>=5.0.0' |       typescript: '>=5.0.0' | ||||||
|  |  | ||||||
|  |   vue@3.4.38: | ||||||
|  |     resolution: {integrity: sha512-f0ZgN+mZ5KFgVv9wz0f4OgVKukoXtS3nwET4c2vLBGQR50aI8G0cqbFtLlX9Yiyg3LFGBitruPHt2PxwTduJEw==} | ||||||
|  |     peerDependencies: | ||||||
|  |       typescript: '*' | ||||||
|  |     peerDependenciesMeta: | ||||||
|  |       typescript: | ||||||
|  |         optional: true | ||||||
|  |  | ||||||
|   vue@3.5.11: |   vue@3.5.11: | ||||||
|     resolution: {integrity: sha512-/8Wurrd9J3lb72FTQS7gRMNQD4nztTtKPmuDuPuhqXmmpD6+skVjAeahNpVzsuky6Sy9gy7wn8UadqPtt9SQIg==} |     resolution: {integrity: sha512-/8Wurrd9J3lb72FTQS7gRMNQD4nztTtKPmuDuPuhqXmmpD6+skVjAeahNpVzsuky6Sy9gy7wn8UadqPtt9SQIg==} | ||||||
|     peerDependencies: |     peerDependencies: | ||||||
| @@ -3932,7 +3969,7 @@ snapshots: | |||||||
|  |  | ||||||
|   '@humanwhocodes/retry@0.3.1': {} |   '@humanwhocodes/retry@0.3.1': {} | ||||||
|  |  | ||||||
|   '@intlify/bundle-utils@9.0.0-beta.0(vue-i18n@10.0.4(vue@3.5.11(typescript@5.6.3)))': |   '@intlify/bundle-utils@9.0.0-beta.0(vue-i18n@10.0.4(vue@3.4.38(typescript@5.6.3)))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@intlify/message-compiler': 10.0.0 |       '@intlify/message-compiler': 10.0.0 | ||||||
|       '@intlify/shared': 10.0.0 |       '@intlify/shared': 10.0.0 | ||||||
| @@ -3944,7 +3981,7 @@ snapshots: | |||||||
|       source-map-js: 1.2.1 |       source-map-js: 1.2.1 | ||||||
|       yaml-eslint-parser: 1.2.3 |       yaml-eslint-parser: 1.2.3 | ||||||
|     optionalDependencies: |     optionalDependencies: | ||||||
|       vue-i18n: 10.0.4(vue@3.5.11(typescript@5.6.3)) |       vue-i18n: 10.0.4(vue@3.4.38(typescript@5.6.3)) | ||||||
|  |  | ||||||
|   '@intlify/core-base@10.0.4': |   '@intlify/core-base@10.0.4': | ||||||
|     dependencies: |     dependencies: | ||||||
| @@ -3965,12 +4002,12 @@ snapshots: | |||||||
|  |  | ||||||
|   '@intlify/shared@10.0.4': {} |   '@intlify/shared@10.0.4': {} | ||||||
|  |  | ||||||
|   '@intlify/unplugin-vue-i18n@5.2.0(@vue/compiler-dom@3.5.11)(eslint@9.12.0(jiti@1.21.6))(rollup@4.24.0)(typescript@5.6.3)(vue-i18n@10.0.4(vue@3.5.11(typescript@5.6.3)))(vue@3.5.11(typescript@5.6.3))': |   '@intlify/unplugin-vue-i18n@5.2.0(@vue/compiler-dom@3.5.11)(eslint@9.12.0(jiti@1.21.6))(rollup@4.24.0)(typescript@5.6.3)(vue-i18n@10.0.4(vue@3.4.38(typescript@5.6.3)))(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@eslint-community/eslint-utils': 4.4.0(eslint@9.12.0(jiti@1.21.6)) |       '@eslint-community/eslint-utils': 4.4.0(eslint@9.12.0(jiti@1.21.6)) | ||||||
|       '@intlify/bundle-utils': 9.0.0-beta.0(vue-i18n@10.0.4(vue@3.5.11(typescript@5.6.3))) |       '@intlify/bundle-utils': 9.0.0-beta.0(vue-i18n@10.0.4(vue@3.4.38(typescript@5.6.3))) | ||||||
|       '@intlify/shared': 10.0.0 |       '@intlify/shared': 10.0.0 | ||||||
|       '@intlify/vue-i18n-extensions': 7.0.0(@intlify/shared@10.0.0)(@vue/compiler-dom@3.5.11)(vue-i18n@10.0.4(vue@3.5.11(typescript@5.6.3)))(vue@3.5.11(typescript@5.6.3)) |       '@intlify/vue-i18n-extensions': 7.0.0(@intlify/shared@10.0.0)(@vue/compiler-dom@3.5.11)(vue-i18n@10.0.4(vue@3.4.38(typescript@5.6.3)))(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@rollup/pluginutils': 5.1.2(rollup@4.24.0) |       '@rollup/pluginutils': 5.1.2(rollup@4.24.0) | ||||||
|       '@typescript-eslint/scope-manager': 7.18.0 |       '@typescript-eslint/scope-manager': 7.18.0 | ||||||
|       '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.3) |       '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.3) | ||||||
| @@ -3982,9 +4019,9 @@ snapshots: | |||||||
|       picocolors: 1.1.0 |       picocolors: 1.1.0 | ||||||
|       source-map-js: 1.2.1 |       source-map-js: 1.2.1 | ||||||
|       unplugin: 1.14.1 |       unplugin: 1.14.1 | ||||||
|       vue: 3.5.11(typescript@5.6.3) |       vue: 3.4.38(typescript@5.6.3) | ||||||
|     optionalDependencies: |     optionalDependencies: | ||||||
|       vue-i18n: 10.0.4(vue@3.5.11(typescript@5.6.3)) |       vue-i18n: 10.0.4(vue@3.4.38(typescript@5.6.3)) | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - '@vue/compiler-dom' |       - '@vue/compiler-dom' | ||||||
|       - eslint |       - eslint | ||||||
| @@ -3993,14 +4030,14 @@ snapshots: | |||||||
|       - typescript |       - typescript | ||||||
|       - webpack-sources |       - webpack-sources | ||||||
|  |  | ||||||
|   '@intlify/vue-i18n-extensions@7.0.0(@intlify/shared@10.0.0)(@vue/compiler-dom@3.5.11)(vue-i18n@10.0.4(vue@3.5.11(typescript@5.6.3)))(vue@3.5.11(typescript@5.6.3))': |   '@intlify/vue-i18n-extensions@7.0.0(@intlify/shared@10.0.0)(@vue/compiler-dom@3.5.11)(vue-i18n@10.0.4(vue@3.4.38(typescript@5.6.3)))(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@babel/parser': 7.25.8 |       '@babel/parser': 7.25.8 | ||||||
|     optionalDependencies: |     optionalDependencies: | ||||||
|       '@intlify/shared': 10.0.0 |       '@intlify/shared': 10.0.0 | ||||||
|       '@vue/compiler-dom': 3.5.11 |       '@vue/compiler-dom': 3.5.11 | ||||||
|       vue: 3.5.11(typescript@5.6.3) |       vue: 3.4.38(typescript@5.6.3) | ||||||
|       vue-i18n: 10.0.4(vue@3.5.11(typescript@5.6.3)) |       vue-i18n: 10.0.4(vue@3.4.38(typescript@5.6.3)) | ||||||
|  |  | ||||||
|   '@isaacs/cliui@8.0.2': |   '@isaacs/cliui@8.0.2': | ||||||
|     dependencies: |     dependencies: | ||||||
| @@ -4071,16 +4108,16 @@ snapshots: | |||||||
|     dependencies: |     dependencies: | ||||||
|       '@primevue/metadata': 4.1.0 |       '@primevue/metadata': 4.1.0 | ||||||
|  |  | ||||||
|   '@primevue/core@4.1.0(vue@3.5.11(typescript@5.6.3))': |   '@primevue/core@4.1.0(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@primeuix/styled': 0.2.0 |       '@primeuix/styled': 0.2.0 | ||||||
|       '@primeuix/utils': 0.2.0 |       '@primeuix/utils': 0.2.0 | ||||||
|       vue: 3.5.11(typescript@5.6.3) |       vue: 3.4.38(typescript@5.6.3) | ||||||
|  |  | ||||||
|   '@primevue/icons@4.1.0(vue@3.5.11(typescript@5.6.3))': |   '@primevue/icons@4.1.0(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@primeuix/utils': 0.2.0 |       '@primeuix/utils': 0.2.0 | ||||||
|       '@primevue/core': 4.1.0(vue@3.5.11(typescript@5.6.3)) |       '@primevue/core': 4.1.0(vue@3.4.38(typescript@5.6.3)) | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - vue |       - vue | ||||||
|  |  | ||||||
| @@ -4368,10 +4405,10 @@ snapshots: | |||||||
|       '@typescript-eslint/types': 8.8.1 |       '@typescript-eslint/types': 8.8.1 | ||||||
|       eslint-visitor-keys: 3.4.3 |       eslint-visitor-keys: 3.4.3 | ||||||
|  |  | ||||||
|   '@vitejs/plugin-vue@5.1.4(vite@5.4.8(@types/node@22.7.5))(vue@3.5.11(typescript@5.6.3))': |   '@vitejs/plugin-vue@5.1.4(vite@5.4.8(@types/node@22.7.5))(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       vite: 5.4.8(@types/node@22.7.5) |       vite: 5.4.8(@types/node@22.7.5) | ||||||
|       vue: 3.5.11(typescript@5.6.3) |       vue: 3.4.38(typescript@5.6.3) | ||||||
|  |  | ||||||
|   '@vitest/eslint-plugin@1.1.7(@typescript-eslint/utils@8.8.1(eslint@9.12.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.12.0(jiti@1.21.6))(typescript@5.6.3)': |   '@vitest/eslint-plugin@1.1.7(@typescript-eslint/utils@8.8.1(eslint@9.12.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.12.0(jiti@1.21.6))(typescript@5.6.3)': | ||||||
|     dependencies: |     dependencies: | ||||||
| @@ -4392,42 +4429,55 @@ snapshots: | |||||||
|       path-browserify: 1.0.1 |       path-browserify: 1.0.1 | ||||||
|       vscode-uri: 3.0.8 |       vscode-uri: 3.0.8 | ||||||
|  |  | ||||||
|   '@vue-macros/api@0.11.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3))': |   '@vue-macros/api@0.11.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@babel/types': 7.25.8 |       '@babel/types': 7.25.8 | ||||||
|       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       resolve.exports: 2.0.2 |       resolve.exports: 2.0.2 | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - rollup |       - rollup | ||||||
|       - vue |       - vue | ||||||
|  |  | ||||||
|   '@vue-macros/better-define@1.9.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3))': |   '@vue-macros/better-define@1.9.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue-macros/api': 0.11.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/api': 0.11.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       unplugin: 1.14.1 |       unplugin: 1.14.1 | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - rollup |       - rollup | ||||||
|       - vue |       - vue | ||||||
|       - webpack-sources |       - webpack-sources | ||||||
|  |  | ||||||
|   '@vue-macros/boolean-prop@0.5.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3))': |   '@vue-macros/boolean-prop@0.5.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue/compiler-core': 3.5.11 |       '@vue/compiler-core': 3.5.11 | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - rollup |       - rollup | ||||||
|       - vue |       - vue | ||||||
|  |  | ||||||
|   '@vue-macros/chain-call@0.4.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3))': |   '@vue-macros/chain-call@0.4.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       unplugin: 1.14.1 |       unplugin: 1.14.1 | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - rollup |       - rollup | ||||||
|       - vue |       - vue | ||||||
|       - webpack-sources |       - webpack-sources | ||||||
|  |  | ||||||
|  |   '@vue-macros/common@1.14.0(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3))': | ||||||
|  |     dependencies: | ||||||
|  |       '@babel/types': 7.25.8 | ||||||
|  |       '@rollup/pluginutils': 5.1.2(rollup@4.24.0) | ||||||
|  |       '@vue/compiler-sfc': 3.5.11 | ||||||
|  |       ast-kit: 1.2.1 | ||||||
|  |       local-pkg: 0.5.0 | ||||||
|  |       magic-string-ast: 0.6.2 | ||||||
|  |     optionalDependencies: | ||||||
|  |       vue: 3.4.38(typescript@5.6.3) | ||||||
|  |     transitivePeerDependencies: | ||||||
|  |       - rollup | ||||||
|  |  | ||||||
|   '@vue-macros/common@1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3))': |   '@vue-macros/common@1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@babel/types': 7.25.8 |       '@babel/types': 7.25.8 | ||||||
| @@ -4441,9 +4491,9 @@ snapshots: | |||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - rollup |       - rollup | ||||||
|  |  | ||||||
|   '@vue-macros/config@0.4.2(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3))': |   '@vue-macros/config@0.4.2(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       make-synchronized: 0.2.9 |       make-synchronized: 0.2.9 | ||||||
|       unconfig: 0.5.5 |       unconfig: 0.5.5 | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
| @@ -4451,71 +4501,71 @@ snapshots: | |||||||
|       - supports-color |       - supports-color | ||||||
|       - vue |       - vue | ||||||
|  |  | ||||||
|   '@vue-macros/define-emit@0.4.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3))': |   '@vue-macros/define-emit@0.4.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue-macros/api': 0.11.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/api': 0.11.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       unplugin: 1.14.1 |       unplugin: 1.14.1 | ||||||
|       vue: 3.5.11(typescript@5.6.3) |       vue: 3.4.38(typescript@5.6.3) | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - rollup |       - rollup | ||||||
|       - webpack-sources |       - webpack-sources | ||||||
|  |  | ||||||
|   '@vue-macros/define-models@1.3.1(@vueuse/core@11.1.0(vue@3.5.11(typescript@5.6.3)))(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3))': |   '@vue-macros/define-models@1.3.1(@vueuse/core@11.1.0(vue@3.4.38(typescript@5.6.3)))(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       ast-walker-scope: 0.6.2 |       ast-walker-scope: 0.6.2 | ||||||
|       unplugin: 1.14.1 |       unplugin: 1.14.1 | ||||||
|     optionalDependencies: |     optionalDependencies: | ||||||
|       '@vueuse/core': 11.1.0(vue@3.5.11(typescript@5.6.3)) |       '@vueuse/core': 11.1.0(vue@3.4.38(typescript@5.6.3)) | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - rollup |       - rollup | ||||||
|       - vue |       - vue | ||||||
|       - webpack-sources |       - webpack-sources | ||||||
|  |  | ||||||
|   '@vue-macros/define-prop@0.5.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3))': |   '@vue-macros/define-prop@0.5.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue-macros/api': 0.11.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/api': 0.11.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       unplugin: 1.14.1 |       unplugin: 1.14.1 | ||||||
|       vue: 3.5.11(typescript@5.6.3) |       vue: 3.4.38(typescript@5.6.3) | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - rollup |       - rollup | ||||||
|       - webpack-sources |       - webpack-sources | ||||||
|  |  | ||||||
|   '@vue-macros/define-props-refs@1.3.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3))': |   '@vue-macros/define-props-refs@1.3.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       unplugin: 1.14.1 |       unplugin: 1.14.1 | ||||||
|       vue: 3.5.11(typescript@5.6.3) |       vue: 3.4.38(typescript@5.6.3) | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - rollup |       - rollup | ||||||
|       - webpack-sources |       - webpack-sources | ||||||
|  |  | ||||||
|   '@vue-macros/define-props@4.0.1(@vue-macros/reactivity-transform@1.1.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)))(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3))': |   '@vue-macros/define-props@4.0.1(@vue-macros/reactivity-transform@1.1.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)))(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/reactivity-transform': 1.1.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/reactivity-transform': 1.1.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       unplugin: 1.14.1 |       unplugin: 1.14.1 | ||||||
|       vue: 3.5.11(typescript@5.6.3) |       vue: 3.4.38(typescript@5.6.3) | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - rollup |       - rollup | ||||||
|       - webpack-sources |       - webpack-sources | ||||||
|  |  | ||||||
|   '@vue-macros/define-render@1.6.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3))': |   '@vue-macros/define-render@1.6.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       unplugin: 1.14.1 |       unplugin: 1.14.1 | ||||||
|       vue: 3.5.11(typescript@5.6.3) |       vue: 3.4.38(typescript@5.6.3) | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - rollup |       - rollup | ||||||
|       - webpack-sources |       - webpack-sources | ||||||
|  |  | ||||||
|   '@vue-macros/define-slots@1.2.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3))': |   '@vue-macros/define-slots@1.2.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       unplugin: 1.14.1 |       unplugin: 1.14.1 | ||||||
|       vue: 3.5.11(typescript@5.6.3) |       vue: 3.4.38(typescript@5.6.3) | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - rollup |       - rollup | ||||||
|       - webpack-sources |       - webpack-sources | ||||||
| @@ -4529,37 +4579,37 @@ snapshots: | |||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - typescript |       - typescript | ||||||
|  |  | ||||||
|   '@vue-macros/export-expose@0.3.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3))': |   '@vue-macros/export-expose@0.3.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue/compiler-sfc': 3.5.11 |       '@vue/compiler-sfc': 3.5.11 | ||||||
|       unplugin: 1.14.1 |       unplugin: 1.14.1 | ||||||
|       vue: 3.5.11(typescript@5.6.3) |       vue: 3.4.38(typescript@5.6.3) | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - rollup |       - rollup | ||||||
|       - webpack-sources |       - webpack-sources | ||||||
|  |  | ||||||
|   '@vue-macros/export-props@0.6.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3))': |   '@vue-macros/export-props@0.6.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       unplugin: 1.14.1 |       unplugin: 1.14.1 | ||||||
|       vue: 3.5.11(typescript@5.6.3) |       vue: 3.4.38(typescript@5.6.3) | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - rollup |       - rollup | ||||||
|       - webpack-sources |       - webpack-sources | ||||||
|  |  | ||||||
|   '@vue-macros/export-render@0.3.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3))': |   '@vue-macros/export-render@0.3.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       unplugin: 1.14.1 |       unplugin: 1.14.1 | ||||||
|       vue: 3.5.11(typescript@5.6.3) |       vue: 3.4.38(typescript@5.6.3) | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - rollup |       - rollup | ||||||
|       - webpack-sources |       - webpack-sources | ||||||
|  |  | ||||||
|   '@vue-macros/hoist-static@1.6.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3))': |   '@vue-macros/hoist-static@1.6.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       unplugin: 1.14.1 |       unplugin: 1.14.1 | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - rollup |       - rollup | ||||||
| @@ -4576,9 +4626,9 @@ snapshots: | |||||||
|       - typescript |       - typescript | ||||||
|       - webpack-sources |       - webpack-sources | ||||||
|  |  | ||||||
|   '@vue-macros/named-template@0.5.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3))': |   '@vue-macros/named-template@0.5.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue/compiler-dom': 3.5.11 |       '@vue/compiler-dom': 3.5.11 | ||||||
|       unplugin: 1.14.1 |       unplugin: 1.14.1 | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
| @@ -4586,31 +4636,31 @@ snapshots: | |||||||
|       - vue |       - vue | ||||||
|       - webpack-sources |       - webpack-sources | ||||||
|  |  | ||||||
|   '@vue-macros/reactivity-transform@1.1.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3))': |   '@vue-macros/reactivity-transform@1.1.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@babel/parser': 7.25.8 |       '@babel/parser': 7.25.8 | ||||||
|       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue/compiler-core': 3.5.11 |       '@vue/compiler-core': 3.5.11 | ||||||
|       '@vue/shared': 3.5.11 |       '@vue/shared': 3.5.11 | ||||||
|       magic-string: 0.30.12 |       magic-string: 0.30.12 | ||||||
|       unplugin: 1.14.1 |       unplugin: 1.14.1 | ||||||
|       vue: 3.5.11(typescript@5.6.3) |       vue: 3.4.38(typescript@5.6.3) | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - rollup |       - rollup | ||||||
|       - webpack-sources |       - webpack-sources | ||||||
|  |  | ||||||
|   '@vue-macros/script-lang@0.2.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3))': |   '@vue-macros/script-lang@0.2.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       unplugin: 1.14.1 |       unplugin: 1.14.1 | ||||||
|       vue: 3.5.11(typescript@5.6.3) |       vue: 3.4.38(typescript@5.6.3) | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - rollup |       - rollup | ||||||
|       - webpack-sources |       - webpack-sources | ||||||
|  |  | ||||||
|   '@vue-macros/setup-block@0.4.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3))': |   '@vue-macros/setup-block@0.4.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue/compiler-dom': 3.5.11 |       '@vue/compiler-dom': 3.5.11 | ||||||
|       unplugin: 1.14.1 |       unplugin: 1.14.1 | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
| @@ -4618,56 +4668,56 @@ snapshots: | |||||||
|       - vue |       - vue | ||||||
|       - webpack-sources |       - webpack-sources | ||||||
|  |  | ||||||
|   '@vue-macros/setup-component@0.18.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3))': |   '@vue-macros/setup-component@0.18.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       unplugin: 1.14.1 |       unplugin: 1.14.1 | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - rollup |       - rollup | ||||||
|       - vue |       - vue | ||||||
|       - webpack-sources |       - webpack-sources | ||||||
|  |  | ||||||
|   '@vue-macros/setup-sfc@0.18.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3))': |   '@vue-macros/setup-sfc@0.18.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       unplugin: 1.14.1 |       unplugin: 1.14.1 | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - rollup |       - rollup | ||||||
|       - vue |       - vue | ||||||
|       - webpack-sources |       - webpack-sources | ||||||
|  |  | ||||||
|   '@vue-macros/short-bind@1.1.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3))': |   '@vue-macros/short-bind@1.1.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue/compiler-core': 3.5.11 |       '@vue/compiler-core': 3.5.11 | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - rollup |       - rollup | ||||||
|       - vue |       - vue | ||||||
|  |  | ||||||
|   '@vue-macros/short-emits@1.6.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3))': |   '@vue-macros/short-emits@1.6.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       unplugin: 1.14.1 |       unplugin: 1.14.1 | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - rollup |       - rollup | ||||||
|       - vue |       - vue | ||||||
|       - webpack-sources |       - webpack-sources | ||||||
|  |  | ||||||
|   '@vue-macros/short-vmodel@1.5.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3))': |   '@vue-macros/short-vmodel@1.5.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue/compiler-core': 3.5.11 |       '@vue/compiler-core': 3.5.11 | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - rollup |       - rollup | ||||||
|       - vue |       - vue | ||||||
|  |  | ||||||
|   '@vue-macros/volar@0.30.3(rollup@4.24.0)(typescript@5.6.3)(vue-tsc@2.1.6(typescript@5.6.3))(vue@3.5.11(typescript@5.6.3))': |   '@vue-macros/volar@0.30.3(rollup@4.24.0)(typescript@5.6.3)(vue-tsc@2.1.6(typescript@5.6.3))(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue-macros/boolean-prop': 0.5.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/boolean-prop': 0.5.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/config': 0.4.2(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/config': 0.4.2(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/short-bind': 1.1.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/short-bind': 1.1.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/short-vmodel': 1.5.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/short-vmodel': 1.5.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue/language-core': 2.1.6(typescript@5.6.3) |       '@vue/language-core': 2.1.6(typescript@5.6.3) | ||||||
|       muggle-string: 0.4.1 |       muggle-string: 0.4.1 | ||||||
|     optionalDependencies: |     optionalDependencies: | ||||||
| @@ -4708,6 +4758,14 @@ snapshots: | |||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - supports-color |       - supports-color | ||||||
|  |  | ||||||
|  |   '@vue/compiler-core@3.4.38': | ||||||
|  |     dependencies: | ||||||
|  |       '@babel/parser': 7.25.8 | ||||||
|  |       '@vue/shared': 3.4.38 | ||||||
|  |       entities: 4.5.0 | ||||||
|  |       estree-walker: 2.0.2 | ||||||
|  |       source-map-js: 1.2.1 | ||||||
|  |  | ||||||
|   '@vue/compiler-core@3.5.11': |   '@vue/compiler-core@3.5.11': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@babel/parser': 7.25.8 |       '@babel/parser': 7.25.8 | ||||||
| @@ -4716,11 +4774,28 @@ snapshots: | |||||||
|       estree-walker: 2.0.2 |       estree-walker: 2.0.2 | ||||||
|       source-map-js: 1.2.1 |       source-map-js: 1.2.1 | ||||||
|  |  | ||||||
|  |   '@vue/compiler-dom@3.4.38': | ||||||
|  |     dependencies: | ||||||
|  |       '@vue/compiler-core': 3.4.38 | ||||||
|  |       '@vue/shared': 3.4.38 | ||||||
|  |  | ||||||
|   '@vue/compiler-dom@3.5.11': |   '@vue/compiler-dom@3.5.11': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue/compiler-core': 3.5.11 |       '@vue/compiler-core': 3.5.11 | ||||||
|       '@vue/shared': 3.5.11 |       '@vue/shared': 3.5.11 | ||||||
|  |  | ||||||
|  |   '@vue/compiler-sfc@3.4.38': | ||||||
|  |     dependencies: | ||||||
|  |       '@babel/parser': 7.25.8 | ||||||
|  |       '@vue/compiler-core': 3.4.38 | ||||||
|  |       '@vue/compiler-dom': 3.4.38 | ||||||
|  |       '@vue/compiler-ssr': 3.4.38 | ||||||
|  |       '@vue/shared': 3.4.38 | ||||||
|  |       estree-walker: 2.0.2 | ||||||
|  |       magic-string: 0.30.12 | ||||||
|  |       postcss: 8.4.47 | ||||||
|  |       source-map-js: 1.2.1 | ||||||
|  |  | ||||||
|   '@vue/compiler-sfc@3.5.11': |   '@vue/compiler-sfc@3.5.11': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@babel/parser': 7.25.8 |       '@babel/parser': 7.25.8 | ||||||
| @@ -4733,6 +4808,11 @@ snapshots: | |||||||
|       postcss: 8.4.47 |       postcss: 8.4.47 | ||||||
|       source-map-js: 1.2.1 |       source-map-js: 1.2.1 | ||||||
|  |  | ||||||
|  |   '@vue/compiler-ssr@3.4.38': | ||||||
|  |     dependencies: | ||||||
|  |       '@vue/compiler-dom': 3.4.38 | ||||||
|  |       '@vue/shared': 3.4.38 | ||||||
|  |  | ||||||
|   '@vue/compiler-ssr@3.5.11': |   '@vue/compiler-ssr@3.5.11': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue/compiler-dom': 3.5.11 |       '@vue/compiler-dom': 3.5.11 | ||||||
| @@ -4745,7 +4825,7 @@ snapshots: | |||||||
|  |  | ||||||
|   '@vue/devtools-api@6.6.4': {} |   '@vue/devtools-api@6.6.4': {} | ||||||
|  |  | ||||||
|   '@vue/devtools-core@7.4.6(vite@5.4.8(@types/node@22.7.5))(vue@3.5.11(typescript@5.6.3))': |   '@vue/devtools-core@7.4.6(vite@5.4.8(@types/node@22.7.5))(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue/devtools-kit': 7.4.6 |       '@vue/devtools-kit': 7.4.6 | ||||||
|       '@vue/devtools-shared': 7.4.6 |       '@vue/devtools-shared': 7.4.6 | ||||||
| @@ -4753,7 +4833,7 @@ snapshots: | |||||||
|       nanoid: 3.3.7 |       nanoid: 3.3.7 | ||||||
|       pathe: 1.1.2 |       pathe: 1.1.2 | ||||||
|       vite-hot-client: 0.2.3(vite@5.4.8(@types/node@22.7.5)) |       vite-hot-client: 0.2.3(vite@5.4.8(@types/node@22.7.5)) | ||||||
|       vue: 3.5.11(typescript@5.6.3) |       vue: 3.4.38(typescript@5.6.3) | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - vite |       - vite | ||||||
|  |  | ||||||
| @@ -4784,15 +4864,31 @@ snapshots: | |||||||
|     optionalDependencies: |     optionalDependencies: | ||||||
|       typescript: 5.6.3 |       typescript: 5.6.3 | ||||||
|  |  | ||||||
|  |   '@vue/reactivity@3.4.38': | ||||||
|  |     dependencies: | ||||||
|  |       '@vue/shared': 3.4.38 | ||||||
|  |  | ||||||
|   '@vue/reactivity@3.5.11': |   '@vue/reactivity@3.5.11': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue/shared': 3.5.11 |       '@vue/shared': 3.5.11 | ||||||
|  |  | ||||||
|  |   '@vue/runtime-core@3.4.38': | ||||||
|  |     dependencies: | ||||||
|  |       '@vue/reactivity': 3.4.38 | ||||||
|  |       '@vue/shared': 3.4.38 | ||||||
|  |  | ||||||
|   '@vue/runtime-core@3.5.11': |   '@vue/runtime-core@3.5.11': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue/reactivity': 3.5.11 |       '@vue/reactivity': 3.5.11 | ||||||
|       '@vue/shared': 3.5.11 |       '@vue/shared': 3.5.11 | ||||||
|  |  | ||||||
|  |   '@vue/runtime-dom@3.4.38': | ||||||
|  |     dependencies: | ||||||
|  |       '@vue/reactivity': 3.4.38 | ||||||
|  |       '@vue/runtime-core': 3.4.38 | ||||||
|  |       '@vue/shared': 3.4.38 | ||||||
|  |       csstype: 3.1.3 | ||||||
|  |  | ||||||
|   '@vue/runtime-dom@3.5.11': |   '@vue/runtime-dom@3.5.11': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue/reactivity': 3.5.11 |       '@vue/reactivity': 3.5.11 | ||||||
| @@ -4800,29 +4896,37 @@ snapshots: | |||||||
|       '@vue/shared': 3.5.11 |       '@vue/shared': 3.5.11 | ||||||
|       csstype: 3.1.3 |       csstype: 3.1.3 | ||||||
|  |  | ||||||
|  |   '@vue/server-renderer@3.4.38(vue@3.4.38(typescript@5.6.3))': | ||||||
|  |     dependencies: | ||||||
|  |       '@vue/compiler-ssr': 3.4.38 | ||||||
|  |       '@vue/shared': 3.4.38 | ||||||
|  |       vue: 3.4.38(typescript@5.6.3) | ||||||
|  |  | ||||||
|   '@vue/server-renderer@3.5.11(vue@3.5.11(typescript@5.6.3))': |   '@vue/server-renderer@3.5.11(vue@3.5.11(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue/compiler-ssr': 3.5.11 |       '@vue/compiler-ssr': 3.5.11 | ||||||
|       '@vue/shared': 3.5.11 |       '@vue/shared': 3.5.11 | ||||||
|       vue: 3.5.11(typescript@5.6.3) |       vue: 3.5.11(typescript@5.6.3) | ||||||
|  |  | ||||||
|  |   '@vue/shared@3.4.38': {} | ||||||
|  |  | ||||||
|   '@vue/shared@3.5.11': {} |   '@vue/shared@3.5.11': {} | ||||||
|  |  | ||||||
|   '@vueuse/core@11.1.0(vue@3.5.11(typescript@5.6.3))': |   '@vueuse/core@11.1.0(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@types/web-bluetooth': 0.0.20 |       '@types/web-bluetooth': 0.0.20 | ||||||
|       '@vueuse/metadata': 11.1.0 |       '@vueuse/metadata': 11.1.0 | ||||||
|       '@vueuse/shared': 11.1.0(vue@3.5.11(typescript@5.6.3)) |       '@vueuse/shared': 11.1.0(vue@3.4.38(typescript@5.6.3)) | ||||||
|       vue-demi: 0.14.10(vue@3.5.11(typescript@5.6.3)) |       vue-demi: 0.14.10(vue@3.4.38(typescript@5.6.3)) | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - '@vue/composition-api' |       - '@vue/composition-api' | ||||||
|       - vue |       - vue | ||||||
|  |  | ||||||
|   '@vueuse/metadata@11.1.0': {} |   '@vueuse/metadata@11.1.0': {} | ||||||
|  |  | ||||||
|   '@vueuse/shared@11.1.0(vue@3.5.11(typescript@5.6.3))': |   '@vueuse/shared@11.1.0(vue@3.4.38(typescript@5.6.3))': | ||||||
|     dependencies: |     dependencies: | ||||||
|       vue-demi: 0.14.10(vue@3.5.11(typescript@5.6.3)) |       vue-demi: 0.14.10(vue@3.4.38(typescript@5.6.3)) | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - '@vue/composition-api' |       - '@vue/composition-api' | ||||||
|       - vue |       - vue | ||||||
| @@ -6322,11 +6426,11 @@ snapshots: | |||||||
|  |  | ||||||
|   pify@2.3.0: {} |   pify@2.3.0: {} | ||||||
|  |  | ||||||
|   pinia@2.2.4(typescript@5.6.3)(vue@3.5.11(typescript@5.6.3)): |   pinia@2.2.4(typescript@5.6.3)(vue@3.4.38(typescript@5.6.3)): | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue/devtools-api': 6.6.4 |       '@vue/devtools-api': 6.6.4 | ||||||
|       vue: 3.5.11(typescript@5.6.3) |       vue: 3.4.38(typescript@5.6.3) | ||||||
|       vue-demi: 0.14.10(vue@3.5.11(typescript@5.6.3)) |       vue-demi: 0.14.10(vue@3.4.38(typescript@5.6.3)) | ||||||
|     optionalDependencies: |     optionalDependencies: | ||||||
|       typescript: 5.6.3 |       typescript: 5.6.3 | ||||||
|  |  | ||||||
| @@ -6389,12 +6493,12 @@ snapshots: | |||||||
|  |  | ||||||
|   primeicons@7.0.0: {} |   primeicons@7.0.0: {} | ||||||
|  |  | ||||||
|   primevue@4.1.0(vue@3.5.11(typescript@5.6.3)): |   primevue@4.1.0(vue@3.4.38(typescript@5.6.3)): | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@primeuix/styled': 0.2.0 |       '@primeuix/styled': 0.2.0 | ||||||
|       '@primeuix/utils': 0.2.0 |       '@primeuix/utils': 0.2.0 | ||||||
|       '@primevue/core': 4.1.0(vue@3.5.11(typescript@5.6.3)) |       '@primevue/core': 4.1.0(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@primevue/icons': 4.1.0(vue@3.5.11(typescript@5.6.3)) |       '@primevue/icons': 4.1.0(vue@3.4.38(typescript@5.6.3)) | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - vue |       - vue | ||||||
|  |  | ||||||
| @@ -6772,7 +6876,7 @@ snapshots: | |||||||
|  |  | ||||||
|   universalify@2.0.1: {} |   universalify@2.0.1: {} | ||||||
|  |  | ||||||
|   unplugin-auto-import@0.18.3(@vueuse/core@11.1.0(vue@3.5.11(typescript@5.6.3)))(rollup@4.24.0): |   unplugin-auto-import@0.18.3(@vueuse/core@11.1.0(vue@3.4.38(typescript@5.6.3)))(rollup@4.24.0): | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@antfu/utils': 0.7.10 |       '@antfu/utils': 0.7.10 | ||||||
|       '@rollup/pluginutils': 5.1.2(rollup@4.24.0) |       '@rollup/pluginutils': 5.1.2(rollup@4.24.0) | ||||||
| @@ -6783,7 +6887,7 @@ snapshots: | |||||||
|       unimport: 3.13.1(rollup@4.24.0) |       unimport: 3.13.1(rollup@4.24.0) | ||||||
|       unplugin: 1.14.1 |       unplugin: 1.14.1 | ||||||
|     optionalDependencies: |     optionalDependencies: | ||||||
|       '@vueuse/core': 11.1.0(vue@3.5.11(typescript@5.6.3)) |       '@vueuse/core': 11.1.0(vue@3.4.38(typescript@5.6.3)) | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - rollup |       - rollup | ||||||
|       - webpack-sources |       - webpack-sources | ||||||
| @@ -6799,7 +6903,7 @@ snapshots: | |||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - webpack-sources |       - webpack-sources | ||||||
|  |  | ||||||
|   unplugin-vue-components@0.27.4(@babel/parser@7.25.8)(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)): |   unplugin-vue-components@0.27.4(@babel/parser@7.25.8)(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)): | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@antfu/utils': 0.7.10 |       '@antfu/utils': 0.7.10 | ||||||
|       '@rollup/pluginutils': 5.1.2(rollup@4.24.0) |       '@rollup/pluginutils': 5.1.2(rollup@4.24.0) | ||||||
| @@ -6811,7 +6915,7 @@ snapshots: | |||||||
|       minimatch: 9.0.5 |       minimatch: 9.0.5 | ||||||
|       mlly: 1.7.2 |       mlly: 1.7.2 | ||||||
|       unplugin: 1.14.1 |       unplugin: 1.14.1 | ||||||
|       vue: 3.5.11(typescript@5.6.3) |       vue: 3.4.38(typescript@5.6.3) | ||||||
|     optionalDependencies: |     optionalDependencies: | ||||||
|       '@babel/parser': 7.25.8 |       '@babel/parser': 7.25.8 | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
| @@ -6819,9 +6923,9 @@ snapshots: | |||||||
|       - supports-color |       - supports-color | ||||||
|       - webpack-sources |       - webpack-sources | ||||||
|  |  | ||||||
|   unplugin-vue-define-options@1.5.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)): |   unplugin-vue-define-options@1.5.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)): | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       ast-walker-scope: 0.6.2 |       ast-walker-scope: 0.6.2 | ||||||
|       unplugin: 1.14.1 |       unplugin: 1.14.1 | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
| @@ -6829,40 +6933,40 @@ snapshots: | |||||||
|       - vue |       - vue | ||||||
|       - webpack-sources |       - webpack-sources | ||||||
|  |  | ||||||
|   unplugin-vue-macros@2.12.3(@vueuse/core@11.1.0(vue@3.5.11(typescript@5.6.3)))(esbuild@0.23.1)(rollup@4.24.0)(typescript@5.6.3)(vite@5.4.8(@types/node@22.7.5))(vue-tsc@2.1.6(typescript@5.6.3))(vue@3.5.11(typescript@5.6.3)): |   unplugin-vue-macros@2.12.3(@vueuse/core@11.1.0(vue@3.4.38(typescript@5.6.3)))(esbuild@0.23.1)(rollup@4.24.0)(typescript@5.6.3)(vite@5.4.8(@types/node@22.7.5))(vue-tsc@2.1.6(typescript@5.6.3))(vue@3.4.38(typescript@5.6.3)): | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue-macros/better-define': 1.9.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/better-define': 1.9.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/boolean-prop': 0.5.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/boolean-prop': 0.5.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/chain-call': 0.4.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/chain-call': 0.4.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/config': 0.4.2(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/config': 0.4.2(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/define-emit': 0.4.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/define-emit': 0.4.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/define-models': 1.3.1(@vueuse/core@11.1.0(vue@3.5.11(typescript@5.6.3)))(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/define-models': 1.3.1(@vueuse/core@11.1.0(vue@3.4.38(typescript@5.6.3)))(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/define-prop': 0.5.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/define-prop': 0.5.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/define-props': 4.0.1(@vue-macros/reactivity-transform@1.1.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)))(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/define-props': 4.0.1(@vue-macros/reactivity-transform@1.1.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)))(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/define-props-refs': 1.3.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/define-props-refs': 1.3.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/define-render': 1.6.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/define-render': 1.6.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/define-slots': 1.2.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/define-slots': 1.2.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/devtools': 0.4.0(typescript@5.6.3)(vite@5.4.8(@types/node@22.7.5)) |       '@vue-macros/devtools': 0.4.0(typescript@5.6.3)(vite@5.4.8(@types/node@22.7.5)) | ||||||
|       '@vue-macros/export-expose': 0.3.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/export-expose': 0.3.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/export-props': 0.6.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/export-props': 0.6.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/export-render': 0.3.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/export-render': 0.3.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/hoist-static': 1.6.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/hoist-static': 1.6.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/jsx-directive': 0.9.1(rollup@4.24.0)(typescript@5.6.3) |       '@vue-macros/jsx-directive': 0.9.1(rollup@4.24.0)(typescript@5.6.3) | ||||||
|       '@vue-macros/named-template': 0.5.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/named-template': 0.5.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/reactivity-transform': 1.1.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/reactivity-transform': 1.1.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/script-lang': 0.2.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/script-lang': 0.2.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/setup-block': 0.4.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/setup-block': 0.4.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/setup-component': 0.18.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/setup-component': 0.18.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/setup-sfc': 0.18.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/setup-sfc': 0.18.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/short-bind': 1.1.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/short-bind': 1.1.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/short-emits': 1.6.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/short-emits': 1.6.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/short-vmodel': 1.5.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/short-vmodel': 1.5.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue-macros/volar': 0.30.3(rollup@4.24.0)(typescript@5.6.3)(vue-tsc@2.1.6(typescript@5.6.3))(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/volar': 0.30.3(rollup@4.24.0)(typescript@5.6.3)(vue-tsc@2.1.6(typescript@5.6.3))(vue@3.4.38(typescript@5.6.3)) | ||||||
|       unplugin: 1.14.1 |       unplugin: 1.14.1 | ||||||
|       unplugin-combine: 1.0.3(esbuild@0.23.1)(rollup@4.24.0)(vite@5.4.8(@types/node@22.7.5)) |       unplugin-combine: 1.0.3(esbuild@0.23.1)(rollup@4.24.0)(vite@5.4.8(@types/node@22.7.5)) | ||||||
|       unplugin-vue-define-options: 1.5.1(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       unplugin-vue-define-options: 1.5.1(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       vue: 3.5.11(typescript@5.6.3) |       vue: 3.4.38(typescript@5.6.3) | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - '@rspack/core' |       - '@rspack/core' | ||||||
|       - '@vueuse/core' |       - '@vueuse/core' | ||||||
| @@ -6890,11 +6994,11 @@ snapshots: | |||||||
|       - rollup |       - rollup | ||||||
|       - webpack-sources |       - webpack-sources | ||||||
|  |  | ||||||
|   unplugin-vue-router@0.10.8(rollup@4.24.0)(vue-router@4.4.5(vue@3.5.11(typescript@5.6.3)))(vue@3.5.11(typescript@5.6.3)): |   unplugin-vue-router@0.10.8(rollup@4.24.0)(vue-router@4.4.5(vue@3.4.38(typescript@5.6.3)))(vue@3.4.38(typescript@5.6.3)): | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@babel/types': 7.25.8 |       '@babel/types': 7.25.8 | ||||||
|       '@rollup/pluginutils': 5.1.2(rollup@4.24.0) |       '@rollup/pluginutils': 5.1.2(rollup@4.24.0) | ||||||
|       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3)) |       '@vue-macros/common': 1.14.0(rollup@4.24.0)(vue@3.4.38(typescript@5.6.3)) | ||||||
|       ast-walker-scope: 0.6.2 |       ast-walker-scope: 0.6.2 | ||||||
|       chokidar: 3.6.0 |       chokidar: 3.6.0 | ||||||
|       fast-glob: 3.3.2 |       fast-glob: 3.3.2 | ||||||
| @@ -6907,7 +7011,7 @@ snapshots: | |||||||
|       unplugin: 1.14.1 |       unplugin: 1.14.1 | ||||||
|       yaml: 2.5.1 |       yaml: 2.5.1 | ||||||
|     optionalDependencies: |     optionalDependencies: | ||||||
|       vue-router: 4.4.5(vue@3.5.11(typescript@5.6.3)) |       vue-router: 4.4.5(vue@3.4.38(typescript@5.6.3)) | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - rollup |       - rollup | ||||||
|       - vue |       - vue | ||||||
| @@ -6957,9 +7061,9 @@ snapshots: | |||||||
|       - rollup |       - rollup | ||||||
|       - supports-color |       - supports-color | ||||||
|  |  | ||||||
|   vite-plugin-vue-devtools@7.4.6(rollup@4.24.0)(vite@5.4.8(@types/node@22.7.5))(vue@3.5.11(typescript@5.6.3)): |   vite-plugin-vue-devtools@7.4.6(rollup@4.24.0)(vite@5.4.8(@types/node@22.7.5))(vue@3.4.38(typescript@5.6.3)): | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue/devtools-core': 7.4.6(vite@5.4.8(@types/node@22.7.5))(vue@3.5.11(typescript@5.6.3)) |       '@vue/devtools-core': 7.4.6(vite@5.4.8(@types/node@22.7.5))(vue@3.4.38(typescript@5.6.3)) | ||||||
|       '@vue/devtools-kit': 7.4.6 |       '@vue/devtools-kit': 7.4.6 | ||||||
|       '@vue/devtools-shared': 7.4.6 |       '@vue/devtools-shared': 7.4.6 | ||||||
|       execa: 8.0.1 |       execa: 8.0.1 | ||||||
| @@ -6988,13 +7092,13 @@ snapshots: | |||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - supports-color |       - supports-color | ||||||
|  |  | ||||||
|   vite-plugin-vue-layouts@0.11.0(vite@5.4.8(@types/node@22.7.5))(vue-router@4.4.5(vue@3.5.11(typescript@5.6.3)))(vue@3.5.11(typescript@5.6.3)): |   vite-plugin-vue-layouts@0.11.0(vite@5.4.8(@types/node@22.7.5))(vue-router@4.4.5(vue@3.4.38(typescript@5.6.3)))(vue@3.4.38(typescript@5.6.3)): | ||||||
|     dependencies: |     dependencies: | ||||||
|       debug: 4.3.7 |       debug: 4.3.7 | ||||||
|       fast-glob: 3.3.2 |       fast-glob: 3.3.2 | ||||||
|       vite: 5.4.8(@types/node@22.7.5) |       vite: 5.4.8(@types/node@22.7.5) | ||||||
|       vue: 3.5.11(typescript@5.6.3) |       vue: 3.4.38(typescript@5.6.3) | ||||||
|       vue-router: 4.4.5(vue@3.5.11(typescript@5.6.3)) |       vue-router: 4.4.5(vue@3.4.38(typescript@5.6.3)) | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - supports-color |       - supports-color | ||||||
|  |  | ||||||
| @@ -7009,9 +7113,9 @@ snapshots: | |||||||
|  |  | ||||||
|   vscode-uri@3.0.8: {} |   vscode-uri@3.0.8: {} | ||||||
|  |  | ||||||
|   vue-demi@0.14.10(vue@3.5.11(typescript@5.6.3)): |   vue-demi@0.14.10(vue@3.4.38(typescript@5.6.3)): | ||||||
|     dependencies: |     dependencies: | ||||||
|       vue: 3.5.11(typescript@5.6.3) |       vue: 3.4.38(typescript@5.6.3) | ||||||
|  |  | ||||||
|   vue-eslint-parser@9.4.3(eslint@9.12.0(jiti@1.21.6)): |   vue-eslint-parser@9.4.3(eslint@9.12.0(jiti@1.21.6)): | ||||||
|     dependencies: |     dependencies: | ||||||
| @@ -7026,17 +7130,17 @@ snapshots: | |||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - supports-color |       - supports-color | ||||||
|  |  | ||||||
|   vue-i18n@10.0.4(vue@3.5.11(typescript@5.6.3)): |   vue-i18n@10.0.4(vue@3.4.38(typescript@5.6.3)): | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@intlify/core-base': 10.0.4 |       '@intlify/core-base': 10.0.4 | ||||||
|       '@intlify/shared': 10.0.4 |       '@intlify/shared': 10.0.4 | ||||||
|       '@vue/devtools-api': 6.6.4 |       '@vue/devtools-api': 6.6.4 | ||||||
|       vue: 3.5.11(typescript@5.6.3) |       vue: 3.4.38(typescript@5.6.3) | ||||||
|  |  | ||||||
|   vue-router@4.4.5(vue@3.5.11(typescript@5.6.3)): |   vue-router@4.4.5(vue@3.4.38(typescript@5.6.3)): | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue/devtools-api': 6.6.4 |       '@vue/devtools-api': 6.6.4 | ||||||
|       vue: 3.5.11(typescript@5.6.3) |       vue: 3.4.38(typescript@5.6.3) | ||||||
|  |  | ||||||
|   vue-tsc@2.1.6(typescript@5.6.3): |   vue-tsc@2.1.6(typescript@5.6.3): | ||||||
|     dependencies: |     dependencies: | ||||||
| @@ -7045,6 +7149,16 @@ snapshots: | |||||||
|       semver: 7.6.3 |       semver: 7.6.3 | ||||||
|       typescript: 5.6.3 |       typescript: 5.6.3 | ||||||
|  |  | ||||||
|  |   vue@3.4.38(typescript@5.6.3): | ||||||
|  |     dependencies: | ||||||
|  |       '@vue/compiler-dom': 3.4.38 | ||||||
|  |       '@vue/compiler-sfc': 3.4.38 | ||||||
|  |       '@vue/runtime-dom': 3.4.38 | ||||||
|  |       '@vue/server-renderer': 3.4.38(vue@3.4.38(typescript@5.6.3)) | ||||||
|  |       '@vue/shared': 3.4.38 | ||||||
|  |     optionalDependencies: | ||||||
|  |       typescript: 5.6.3 | ||||||
|  |  | ||||||
|   vue@3.5.11(typescript@5.6.3): |   vue@3.5.11(typescript@5.6.3): | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@vue/compiler-dom': 3.5.11 |       '@vue/compiler-dom': 3.5.11 | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| [package] | [package] | ||||||
| name = "easytier-gui" | name = "easytier-gui" | ||||||
| version = "2.0.3" | version = "2.1.1" | ||||||
| description = "EasyTier GUI" | description = "EasyTier GUI" | ||||||
| authors = ["you"] | authors = ["you"] | ||||||
| edition = "2021" | edition = "2021" | ||||||
| @@ -15,10 +15,12 @@ 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 = [ | # wry 0.47 may crash on android, see https://github.com/EasyTier/EasyTier/issues/527 | ||||||
|  | tauri = { version = "=2.0.6", 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 +39,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.
										
									
								
							| @@ -39,7 +39,7 @@ | |||||||
|     "vpnservice:allow-prepare-vpn", |     "vpnservice:allow-prepare-vpn", | ||||||
|     "vpnservice:allow-start-vpn", |     "vpnservice:allow-start-vpn", | ||||||
|     "vpnservice:allow-stop-vpn", |     "vpnservice:allow-stop-vpn", | ||||||
|     "vpnservice:allow-register-listener", |     "vpnservice:allow-registerListener", | ||||||
|     "os:default", |     "os:default", | ||||||
|     "os:allow-os-type", |     "os:allow-os-type", | ||||||
|     "os:allow-arch", |     "os:allow-arch", | ||||||
|   | |||||||
| @@ -3,181 +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, |  | ||||||
|     network_length: i32, |  | ||||||
|     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 { |  | ||||||
|                 let ip = format!("{}/{}", self.virtual_ipv4, self.network_length) |  | ||||||
|                     .parse() |  | ||||||
|                     .with_context(|| { |  | ||||||
|                         format!( |  | ||||||
|                             "failed to parse ipv4 inet address: {}, {}", |  | ||||||
|                             self.virtual_ipv4, self.network_length |  | ||||||
|                         ) |  | ||||||
|                     })?; |  | ||||||
|                 cfg.set_ipv4(Some(ip)); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         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); | ||||||
|  |  | ||||||
| @@ -205,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); | ||||||
| @@ -302,7 +141,6 @@ pub fn run() { | |||||||
|         process::exit(0); |         process::exit(0); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     #[cfg(not(target_os = "android"))] |  | ||||||
|     utils::setup_panic_handler(); |     utils::setup_panic_handler(); | ||||||
|  |  | ||||||
|     let mut builder = tauri::Builder::default(); |     let mut builder = tauri::Builder::default(); | ||||||
| @@ -335,7 +173,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 { | ||||||
| @@ -394,6 +232,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.3", |   "version": "2.1.1", | ||||||
|   "identifier": "com.kkrainbow.easytier", |   "identifier": "com.kkrainbow.easytier", | ||||||
|   "plugins": {}, |   "plugins": {}, | ||||||
|   "app": { |   "app": { | ||||||
|   | |||||||
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										2
									
								
								easytier-gui/src/auto-imports.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								easytier-gui/src/auto-imports.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -154,8 +154,6 @@ declare module 'vue' { | |||||||
|     readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']> |     readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']> | ||||||
|     readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']> |     readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']> | ||||||
|     readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']> |     readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']> | ||||||
|     readonly num2ipv4: UnwrapRef<typeof import('./composables/utils')['num2ipv4']> |  | ||||||
|     readonly num2ipv6: UnwrapRef<typeof import('./composables/utils')['num2ipv6']> |  | ||||||
|     readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']> |     readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']> | ||||||
|     readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']> |     readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']> | ||||||
|     readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']> |     readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']> | ||||||
|   | |||||||
| @@ -1,318 +0,0 @@ | |||||||
| <script setup lang="ts"> |  | ||||||
| import InputGroup from 'primevue/inputgroup' |  | ||||||
| import InputGroupAddon from 'primevue/inputgroupaddon' |  | ||||||
| import { getOsHostname } from '~/composables/network' |  | ||||||
|  |  | ||||||
| import { NetworkingMethod } from '~/types/network' |  | ||||||
|  |  | ||||||
| const props = defineProps<{ |  | ||||||
|   configInvalid?: boolean |  | ||||||
|   instanceId?: string |  | ||||||
| }>() |  | ||||||
|  |  | ||||||
| defineEmits(['runNetwork']) |  | ||||||
|  |  | ||||||
| const { t } = useI18n() |  | ||||||
|  |  | ||||||
| const networking_methods = ref([ |  | ||||||
|   { value: NetworkingMethod.PublicServer, label: () => t('public_server') }, |  | ||||||
|   { value: NetworkingMethod.Manual, label: () => t('manual') }, |  | ||||||
|   { value: NetworkingMethod.Standalone, label: () => t('standalone') }, |  | ||||||
| ]) |  | ||||||
|  |  | ||||||
| const networkStore = useNetworkStore() |  | ||||||
| const curNetwork = computed(() => { |  | ||||||
|   if (props.instanceId) { |  | ||||||
|     // console.log('instanceId', props.instanceId) |  | ||||||
|     const c = networkStore.networkList.find(n => n.instance_id === props.instanceId) |  | ||||||
|     if (c !== undefined) |  | ||||||
|       return c |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return networkStore.curNetwork |  | ||||||
| }) |  | ||||||
|  |  | ||||||
| const protos: { [proto: string]: number } = { tcp: 11010, udp: 11010, wg: 11011, ws: 11011, wss: 11012 } |  | ||||||
|  |  | ||||||
| function searchUrlSuggestions(e: { query: string }): string[] { |  | ||||||
|   const query = e.query |  | ||||||
|   const ret = [] |  | ||||||
|   // if query match "^\w+:.*", then no proto prefix |  | ||||||
|   if (query.match(/^\w+:.*/)) { |  | ||||||
|     // if query is a valid url, then add to suggestions |  | ||||||
|     try { |  | ||||||
|       // eslint-disable-next-line no-new |  | ||||||
|       new URL(query) |  | ||||||
|       ret.push(query) |  | ||||||
|     } |  | ||||||
|     catch {} |  | ||||||
|   } |  | ||||||
|   else { |  | ||||||
|     for (const proto in protos) { |  | ||||||
|       let item = `${proto}://${query}` |  | ||||||
|       // if query match ":\d+$", then no port suffix |  | ||||||
|       if (!query.match(/:\d+$/)) { |  | ||||||
|         item += `:${protos[proto]}` |  | ||||||
|       } |  | ||||||
|       ret.push(item) |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return ret |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const publicServerSuggestions = ref(['']) |  | ||||||
|  |  | ||||||
| function searchPresetPublicServers(e: { query: string }) { |  | ||||||
|   const presetPublicServers = [ |  | ||||||
|     'tcp://public.easytier.top:11010', |  | ||||||
|   ] |  | ||||||
|  |  | ||||||
|   const query = e.query |  | ||||||
|   // if query is sub string of presetPublicServers, add to suggestions |  | ||||||
|   let ret = presetPublicServers.filter(item => item.includes(query)) |  | ||||||
|   // add additional suggestions |  | ||||||
|   if (query.length > 0) { |  | ||||||
|     ret = ret.concat(searchUrlSuggestions(e)) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   publicServerSuggestions.value = ret |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const peerSuggestions = ref(['']) |  | ||||||
|  |  | ||||||
| function searchPeerSuggestions(e: { query: string }) { |  | ||||||
|   peerSuggestions.value = searchUrlSuggestions(e) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const inetSuggestions = ref(['']) |  | ||||||
|  |  | ||||||
| function searchInetSuggestions(e: { query: string }) { |  | ||||||
|   if (e.query.search('/') >= 0) { |  | ||||||
|     inetSuggestions.value = [e.query] |  | ||||||
|   } else { |  | ||||||
|     const ret = [] |  | ||||||
|     for (let i = 0; i < 32; i++) { |  | ||||||
|       ret.push(`${e.query}/${i}`) |  | ||||||
|     } |  | ||||||
|     inetSuggestions.value = ret |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const listenerSuggestions = ref(['']) |  | ||||||
|  |  | ||||||
| function searchListenerSuggestiong(e: { query: string }) { |  | ||||||
|   const ret = [] |  | ||||||
|  |  | ||||||
|   for (const proto in protos) { |  | ||||||
|     let item = `${proto}://0.0.0.0:` |  | ||||||
|     // if query is a number, use it as port |  | ||||||
|     if (e.query.match(/^\d+$/)) { |  | ||||||
|       item += e.query |  | ||||||
|     } |  | ||||||
|     else { |  | ||||||
|       item += protos[proto] |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (item.includes(e.query)) { |  | ||||||
|       ret.push(item) |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   if (ret.length === 0) { |  | ||||||
|     ret.push(e.query) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   listenerSuggestions.value = ret |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function validateHostname() { |  | ||||||
|   if (curNetwork.value.hostname) { |  | ||||||
|     // eslint no-useless-escape |  | ||||||
|     let name = curNetwork.value.hostname!.replaceAll(/[^\u4E00-\u9FA5a-z0-9\-]*/gi, '') |  | ||||||
|     if (name.length > 32) |  | ||||||
|       name = name.substring(0, 32) |  | ||||||
|  |  | ||||||
|     if (curNetwork.value.hostname !== name) |  | ||||||
|       curNetwork.value.hostname = name |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const osHostname = ref<string>('') |  | ||||||
|  |  | ||||||
| onMounted(async () => { |  | ||||||
|   osHostname.value = await getOsHostname() |  | ||||||
| }) |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <template> |  | ||||||
|   <div class="flex flex-column h-full"> |  | ||||||
|     <div class="flex flex-column"> |  | ||||||
|       <div class="w-10/12 self-center "> |  | ||||||
|         <Panel :header="t('basic_settings')"> |  | ||||||
|           <div class="flex flex-column gap-y-2"> |  | ||||||
|             <div class="flex flex-row gap-x-9 flex-wrap"> |  | ||||||
|               <div class="flex flex-column gap-2 basis-5/12 grow"> |  | ||||||
|                 <div class="flex align-items-center" for="virtual_ip"> |  | ||||||
|                   <label class="mr-2"> {{ t('virtual_ipv4') }} </label> |  | ||||||
|                   <Checkbox v-model="curNetwork.dhcp" input-id="virtual_ip_auto" :binary="true" /> |  | ||||||
|  |  | ||||||
|                   <label for="virtual_ip_auto" class="ml-2"> |  | ||||||
|                     {{ t('virtual_ipv4_dhcp') }} |  | ||||||
|                   </label> |  | ||||||
|                 </div> |  | ||||||
|                 <InputGroup> |  | ||||||
|                   <InputText |  | ||||||
|                     id="virtual_ip" v-model="curNetwork.virtual_ipv4" :disabled="curNetwork.dhcp" |  | ||||||
|                     aria-describedby="virtual_ipv4-help" |  | ||||||
|                   /> |  | ||||||
|                   <InputGroupAddon> |  | ||||||
|                     <span>/</span> |  | ||||||
|                   </InputGroupAddon> |  | ||||||
|                   <InputNumber v-model="curNetwork.network_length" :disabled="curNetwork.dhcp" inputId="horizontal-buttons" showButtons :step="1" mode="decimal" :min="1" :max="32" fluid class="max-w-20"/> |  | ||||||
|                 </InputGroup> |  | ||||||
|               </div> |  | ||||||
|             </div> |  | ||||||
|  |  | ||||||
|             <div class="flex flex-row gap-x-9 flex-wrap"> |  | ||||||
|               <div class="flex flex-column gap-2 basis-5/12 grow"> |  | ||||||
|                 <label for="network_name">{{ t('network_name') }}</label> |  | ||||||
|                 <InputText id="network_name" v-model="curNetwork.network_name" aria-describedby="network_name-help" /> |  | ||||||
|               </div> |  | ||||||
|               <div class="flex flex-column gap-2 basis-5/12 grow"> |  | ||||||
|                 <label for="network_secret">{{ t('network_secret') }}</label> |  | ||||||
|                 <InputText |  | ||||||
|                   id="network_secret" v-model="curNetwork.network_secret" |  | ||||||
|                   aria-describedby="network_secret-help" |  | ||||||
|                 /> |  | ||||||
|               </div> |  | ||||||
|             </div> |  | ||||||
|  |  | ||||||
|             <div class="flex flex-row gap-x-9 flex-wrap"> |  | ||||||
|               <div class="flex flex-column gap-2 basis-5/12 grow"> |  | ||||||
|                 <label for="nm">{{ t('networking_method') }}</label> |  | ||||||
|                 <SelectButton v-model="curNetwork.networking_method" :options="networking_methods" :option-label="(v) => v.label()" option-value="value" /> |  | ||||||
|                 <div class="items-center flex flex-row p-fluid gap-x-1"> |  | ||||||
|                   <AutoComplete |  | ||||||
|                     v-if="curNetwork.networking_method === NetworkingMethod.Manual" id="chips" |  | ||||||
|                     v-model="curNetwork.peer_urls" :placeholder="t('chips_placeholder', ['tcp://8.8.8.8:11010'])" |  | ||||||
|                     class="grow" multiple fluid :suggestions="peerSuggestions" @complete="searchPeerSuggestions" |  | ||||||
|                   /> |  | ||||||
|  |  | ||||||
|                   <AutoComplete |  | ||||||
|                     v-if="curNetwork.networking_method === NetworkingMethod.PublicServer" v-model="curNetwork.public_server_url" |  | ||||||
|                     :suggestions="publicServerSuggestions" :virtual-scroller-options="{ itemSize: 38 }" class="grow" dropdown :complete-on-focus="true" |  | ||||||
|                     @complete="searchPresetPublicServers" |  | ||||||
|                   /> |  | ||||||
|                 </div> |  | ||||||
|               </div> |  | ||||||
|             </div> |  | ||||||
|           </div> |  | ||||||
|         </Panel> |  | ||||||
|  |  | ||||||
|         <Divider /> |  | ||||||
|  |  | ||||||
|         <Panel :header="t('advanced_settings')" toggleable collapsed> |  | ||||||
|           <div class="flex flex-column gap-y-2"> |  | ||||||
|             <div class="flex flex-row gap-x-9 flex-wrap"> |  | ||||||
|               <div class="flex flex-column gap-2 basis-5/12 grow"> |  | ||||||
|                 <div class="flex align-items-center"> |  | ||||||
|                   <Checkbox v-model="curNetwork.latency_first" input-id="use_latency_first" :binary="true" /> |  | ||||||
|                   <label for="use_latency_first" class="ml-2"> {{ t('use_latency_first') }} </label> |  | ||||||
|                 </div> |  | ||||||
|               </div> |  | ||||||
|             </div> |  | ||||||
|  |  | ||||||
|             <div class="flex flex-row gap-x-9 flex-wrap"> |  | ||||||
|               <div class="flex flex-column gap-2 basis-5/12 grow"> |  | ||||||
|                 <label for="hostname">{{ t('hostname') }}</label> |  | ||||||
|                 <InputText |  | ||||||
|                   id="hostname" v-model="curNetwork.hostname" aria-describedby="hostname-help" :format="true" |  | ||||||
|                   :placeholder="t('hostname_placeholder', [osHostname])" @blur="validateHostname" |  | ||||||
|                 /> |  | ||||||
|               </div> |  | ||||||
|             </div> |  | ||||||
|  |  | ||||||
|             <div class="flex flex-row gap-x-9 flex-wrap w-full"> |  | ||||||
|               <div class="flex flex-column gap-2 grow p-fluid"> |  | ||||||
|                 <label for="username">{{ t('proxy_cidrs') }}</label> |  | ||||||
|                 <AutoComplete |  | ||||||
|                   id="subnet-proxy" |  | ||||||
|                   v-model="curNetwork.proxy_cidrs" :placeholder="t('chips_placeholder', ['10.0.0.0/24'])" |  | ||||||
|                   class="w-full" multiple fluid :suggestions="inetSuggestions" @complete="searchInetSuggestions" |  | ||||||
|                 /> |  | ||||||
|               </div> |  | ||||||
|             </div> |  | ||||||
|  |  | ||||||
|             <div class="flex flex-row gap-x-9 flex-wrap "> |  | ||||||
|               <div class="flex flex-column gap-2 grow"> |  | ||||||
|                 <label for="username">VPN Portal</label> |  | ||||||
|                 <ToggleButton |  | ||||||
|                   v-model="curNetwork.enable_vpn_portal" on-icon="pi pi-check" off-icon="pi pi-times" |  | ||||||
|                   :on-label="t('off_text')" :off-label="t('on_text')" class="w-48" |  | ||||||
|                 /> |  | ||||||
|                 <div v-if="curNetwork.enable_vpn_portal" class="items-center flex flex-row gap-x-4"> |  | ||||||
|                   <div class="min-w-64"> |  | ||||||
|                     <InputGroup> |  | ||||||
|                       <InputText |  | ||||||
|                         v-model="curNetwork.vpn_portal_client_network_addr" |  | ||||||
|                         :placeholder="t('vpn_portal_client_network')" |  | ||||||
|                       /> |  | ||||||
|                       <InputGroupAddon> |  | ||||||
|                         <span>/{{ curNetwork.vpn_portal_client_network_len }}</span> |  | ||||||
|                       </InputGroupAddon> |  | ||||||
|                     </InputGroup> |  | ||||||
|  |  | ||||||
|                     <InputNumber |  | ||||||
|                       v-model="curNetwork.vpn_portal_listen_port" :allow-empty="false" |  | ||||||
|                       :format="false" :min="0" :max="65535" class="w-8" fluid |  | ||||||
|                     /> |  | ||||||
|                   </div> |  | ||||||
|                 </div> |  | ||||||
|               </div> |  | ||||||
|             </div> |  | ||||||
|  |  | ||||||
|             <div class="flex flex-row gap-x-9 flex-wrap"> |  | ||||||
|               <div class="flex flex-column gap-2 grow p-fluid"> |  | ||||||
|                 <label for="listener_urls">{{ t('listener_urls') }}</label> |  | ||||||
|                 <AutoComplete |  | ||||||
|                   id="listener_urls" v-model="curNetwork.listener_urls" |  | ||||||
|                   :suggestions="listenerSuggestions" class="w-full" dropdown :complete-on-focus="true" |  | ||||||
|                   :placeholder="t('chips_placeholder', ['tcp://1.1.1.1:11010'])" |  | ||||||
|                   multiple @complete="searchListenerSuggestiong" |  | ||||||
|                 /> |  | ||||||
|               </div> |  | ||||||
|             </div> |  | ||||||
|  |  | ||||||
|             <div class="flex flex-row gap-x-9 flex-wrap"> |  | ||||||
|               <div class="flex flex-column gap-2 basis-5/12 grow"> |  | ||||||
|                 <label for="rpc_port">{{ t('rpc_port') }}</label> |  | ||||||
|                 <InputNumber |  | ||||||
|                   id="rpc_port" v-model="curNetwork.rpc_port" aria-describedby="rpc_port-help" |  | ||||||
|                   :format="false" :min="0" :max="65535" |  | ||||||
|                 /> |  | ||||||
|               </div> |  | ||||||
|             </div> |  | ||||||
|  |  | ||||||
|             <div class="flex flex-row gap-x-9 flex-wrap"> |  | ||||||
|               <div class="flex flex-column gap-2 basis-5/12 grow"> |  | ||||||
|                 <label for="dev_name">{{ t('dev_name') }}</label> |  | ||||||
|                 <InputText |  | ||||||
|                   id="dev_name" v-model="curNetwork.dev_name" aria-describedby="dev_name-help" :format="true" |  | ||||||
|                   :placeholder="t('dev_name_placeholder')" |  | ||||||
|                 /> |  | ||||||
|               </div> |  | ||||||
|             </div> |  | ||||||
|           </div> |  | ||||||
|         </Panel> |  | ||||||
|  |  | ||||||
|         <div class="flex pt-4 justify-content-center"> |  | ||||||
|           <Button |  | ||||||
|             :label="t('run_network')" icon="pi pi-arrow-right" icon-pos="right" :disabled="configInvalid" |  | ||||||
|             @click="$emit('runNetwork', curNetwork)" |  | ||||||
|           /> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
| @@ -1,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() | ||||||
|  |  | ||||||
| @@ -46,9 +49,9 @@ async function doStartVpn(ipv4Addr: string, cidr: number, routes: string[]) { | |||||||
|     return |     return | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   console.log('start vpn') |   console.log('start vpn service', ipv4Addr, cidr, routes) | ||||||
|   const start_ret = await start_vpn({ |   const start_ret = await start_vpn({ | ||||||
|     ipv4Addr: `${ipv4Addr}`, |     ipv4Addr: `${ipv4Addr}/${cidr}`, | ||||||
|     routes, |     routes, | ||||||
|     disallowedApplications: ['com.kkrainbow.easytier'], |     disallowedApplications: ['com.kkrainbow.easytier'], | ||||||
|     mtu: 1300, |     mtu: 1300, | ||||||
| @@ -110,6 +113,7 @@ function getRoutesForVpn(routes: Route[]): string[] { | |||||||
| } | } | ||||||
|  |  | ||||||
| async function onNetworkInstanceChange() { | async function onNetworkInstanceChange() { | ||||||
|  |   console.error('vpn service watch network instance change ids', JSON.stringify(networkStore.networkInstanceIds)) | ||||||
|   const insts = networkStore.networkInstanceIds |   const insts = networkStore.networkInstanceIds | ||||||
|   if (!insts) { |   if (!insts) { | ||||||
|     await doStopVpn() |     await doStopVpn() | ||||||
| @@ -122,19 +126,24 @@ 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 | ||||||
|   const routesChanged = JSON.stringify(routes) !== JSON.stringify(curVpnStatus.routes) |   const routesChanged = JSON.stringify(routes) !== JSON.stringify(curVpnStatus.routes) | ||||||
|  |  | ||||||
|   if (ipChanged || routesChanged) { |   if (ipChanged || routesChanged) { | ||||||
|     console.log('virtual ip changed', JSON.stringify(curVpnStatus), virtual_ip) |     console.info('vpn service virtual ip changed', JSON.stringify(curVpnStatus), virtual_ip) | ||||||
|     try { |     try { | ||||||
|       await doStopVpn() |       await doStopVpn() | ||||||
|     } |     } | ||||||
| @@ -146,7 +155,7 @@ async function onNetworkInstanceChange() { | |||||||
|       await doStartVpn(virtual_ip, 24, routes) |       await doStartVpn(virtual_ip, 24, routes) | ||||||
|     } |     } | ||||||
|     catch (e) { |     catch (e) { | ||||||
|       console.error('start vpn failed, clear all network insts.', e) |       console.error('start vpn service failed, clear all network insts.', e) | ||||||
|       networkStore.clearNetworkInstances() |       networkStore.clearNetworkInstances() | ||||||
|       await retainNetworkInstance(networkStore.networkInstanceIds) |       await retainNetworkInstance(networkStore.networkInstanceIds) | ||||||
|     } |     } | ||||||
| @@ -167,6 +176,7 @@ async function watchNetworkInstance() { | |||||||
|     } |     } | ||||||
|     subscribe_running = false |     subscribe_running = false | ||||||
|   }) |   }) | ||||||
|  |   console.error('vpn service watch network instance') | ||||||
| } | } | ||||||
|  |  | ||||||
| export async function initMobileVpnService() { | export async function initMobileVpnService() { | ||||||
|   | |||||||
| @@ -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 }) | ||||||
|   | |||||||
| @@ -1,15 +0,0 @@ | |||||||
| import { IPv4, IPv6 } from 'ip-num/IPNumber' |  | ||||||
| import type { Ipv4Addr, Ipv6Addr } from '~/types/network' |  | ||||||
|  |  | ||||||
| export function num2ipv4(ip: Ipv4Addr) { |  | ||||||
|   return IPv4.fromNumber(ip.addr) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export function num2ipv6(ip: Ipv6Addr) { |  | ||||||
|   return IPv6.fromBigInt( |  | ||||||
|     (BigInt(ip.part1) << BigInt(96)) |  | ||||||
|     + (BigInt(ip.part2) << BigInt(64)) |  | ||||||
|     + (BigInt(ip.part3) << BigInt(32)) |  | ||||||
|     + BigInt(ip.part4), |  | ||||||
|   ) |  | ||||||
| } |  | ||||||
| @@ -5,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') | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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')), | ||||||
| @@ -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) | ||||||
| @@ -232,7 +250,12 @@ onBeforeMount(async () => { | |||||||
|  |  | ||||||
| onMounted(async () => { | onMounted(async () => { | ||||||
|   if (type() === 'android') { |   if (type() === 'android') { | ||||||
|  |     try { | ||||||
|       await initMobileVpnService() |       await initMobileVpnService() | ||||||
|  |       console.error("easytier init vpn service done") | ||||||
|  |     } catch (e: any) { | ||||||
|  |       console.error("easytier init vpn service failed", e) | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| }) | }) | ||||||
|  |  | ||||||
| @@ -245,7 +268,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 +276,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 +288,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 +354,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] | ||||||
|   | |||||||
| @@ -1,9 +1,11 @@ | |||||||
|  | import { networkInterfaces } from 'node:os' | ||||||
| import path from 'node:path' | import path from 'node:path' | ||||||
| import process from 'node:process' | import process from 'node:process' | ||||||
| import VueI18n from '@intlify/unplugin-vue-i18n/vite' | import VueI18n from '@intlify/unplugin-vue-i18n/vite' | ||||||
| import { PrimeVueResolver } from '@primevue/auto-import-resolver' | import { PrimeVueResolver } from '@primevue/auto-import-resolver' | ||||||
| import Vue from '@vitejs/plugin-vue' | import Vue from '@vitejs/plugin-vue' | ||||||
| import { internalIpV4Sync } from 'internal-ip' | import { containsCidr, parseCidr } from 'cidr-tools' | ||||||
|  | import { gateway4sync } from 'default-gateway' | ||||||
| import AutoImport from 'unplugin-auto-import/vite' | import AutoImport from 'unplugin-auto-import/vite' | ||||||
| import Components from 'unplugin-vue-components/vite' | import Components from 'unplugin-vue-components/vite' | ||||||
| import VueMacros from 'unplugin-vue-macros/vite' | import VueMacros from 'unplugin-vue-macros/vite' | ||||||
| @@ -13,6 +15,20 @@ import { defineConfig } from 'vite' | |||||||
| import VueDevTools from 'vite-plugin-vue-devtools' | import VueDevTools from 'vite-plugin-vue-devtools' | ||||||
| import Layouts from 'vite-plugin-vue-layouts' | import Layouts from 'vite-plugin-vue-layouts' | ||||||
|  |  | ||||||
|  | function findIp(gateway: string) { | ||||||
|  |   // Look for the matching interface in all local interfaces | ||||||
|  |   console.log('gateway', gateway) | ||||||
|  |   for (const addresses of Object.values(networkInterfaces())) { | ||||||
|  |     if (!addresses) | ||||||
|  |       continue | ||||||
|  |     for (const { cidr } of addresses) { | ||||||
|  |       if (cidr && containsCidr(cidr, gateway)) { | ||||||
|  |         return parseCidr(cidr).ip | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| const host = process.env.TAURI_DEV_HOST | const host = process.env.TAURI_DEV_HOST | ||||||
|  |  | ||||||
| // https://vitejs.dev/config/ | // https://vitejs.dev/config/ | ||||||
| @@ -100,7 +116,7 @@ export default defineConfig(async () => ({ | |||||||
|     hmr: host |     hmr: host | ||||||
|       ? { |       ? { | ||||||
|         protocol: 'ws', |         protocol: 'ws', | ||||||
|           host: internalIpV4Sync(), |         host: findIp(gateway4sync().gateway), | ||||||
|         port: 1430, |         port: 1430, | ||||||
|       } |       } | ||||||
|       : undefined, |       : undefined, | ||||||
|   | |||||||
							
								
								
									
										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`.
 | ||||||
| ///
 | ///
 | ||||||
							
								
								
									
										57
									
								
								easytier-web/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								easytier-web/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | |||||||
|  | [package] | ||||||
|  | name = "easytier-web" | ||||||
|  | version = "0.1.0" | ||||||
|  | edition = "2021" | ||||||
|  | description = "Config server for easytier. easytier-core gets config from this and web frontend use it as restful api server." | ||||||
|  |  | ||||||
|  | [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" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | rust-i18n = "3" | ||||||
|  | sys-locale = "0.3" | ||||||
|  | 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", | ||||||
|  | ] } | ||||||
|  |  | ||||||
|  | chrono = { version = "0.4.37", features = ["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> | ||||||
| @@ -1,5 +1,8 @@ | |||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { EventType } from '~/types/network' | import { useI18n } from 'vue-i18n'; | ||||||
|  | import { EventType } from '../types/network' | ||||||
|  | import { computed } from 'vue'; | ||||||
|  | import { Fieldset } from 'primevue'; | ||||||
| 
 | 
 | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
|   event: { |   event: { | ||||||
| @@ -1,41 +1,28 @@ | |||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { useTimeAgo } from '@vueuse/core' | import { useTimeAgo } from '@vueuse/core' | ||||||
| import { IPv4, IPv6 } from 'ip-num/IPNumber' | import { IPv4 } from 'ip-num/IPNumber' | ||||||
| import type { NodeInfo, PeerRoutePair } from '~/types/network' | 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 [] | ||||||
| @@ -116,14 +103,14 @@ function ipFormat(info: PeerRoutePair) { | |||||||
|   const ip = info.route.ipv4_addr |   const ip = info.route.ipv4_addr | ||||||
|   if (typeof ip === 'string') |   if (typeof ip === 'string') | ||||||
|     return ip |     return ip | ||||||
|   return ip ? `${num2ipv4(ip.address)}/${ip.network_length}` : '' |   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 { | ||||||
| @@ -132,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}`, | ||||||
| @@ -151,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) | ||||||
| 
 | 
 | ||||||
| @@ -159,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}: ${num2ipv4(ip)}`, |       label: `Local IPv4 ${idx}: ${ipv4ToString(ip)}`, | ||||||
|       icon: '', |       icon: '', | ||||||
|     } as Chip) |     } as Chip) | ||||||
|   } |   } | ||||||
| @@ -168,7 +155,7 @@ const myNodeInfoChips = computed(() => { | |||||||
|   const local_ipv6s = my_node_info.ips?.interface_ipv6s |   const local_ipv6s = my_node_info.ips?.interface_ipv6s | ||||||
|   for (const [idx, ip] of local_ipv6s?.entries()) { |   for (const [idx, ip] of local_ipv6s?.entries()) { | ||||||
|     chips.push({ |     chips.push({ | ||||||
|       label: `Local IPv6 ${idx}: ${num2ipv6(ip)}`, |       label: `Local IPv6 ${idx}: ${ipv6ToString(ip)}`, | ||||||
|       icon: '', |       icon: '', | ||||||
|     } as Chip) |     } as Chip) | ||||||
|   } |   } | ||||||
| @@ -185,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) | ||||||
|   } |   } | ||||||
| @@ -198,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) | ||||||
|   } |   } | ||||||
| @@ -308,28 +291,29 @@ 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)" class="w-2/3 h-auto"> |     <Dialog v-model:visible="dialogVisible" modal :header="t(dialogHeader)" class="w-2/3 h-auto max-w-full"> | ||||||
|       <ScrollPanel v-if="dialogHeader === 'vpn_portal_config'"> |       <ScrollPanel v-if="dialogHeader === 'vpn_portal_config'"> | ||||||
|         <pre>{{ dialogContent }}</pre> |         <pre>{{ dialogContent }}</pre> | ||||||
|       </ScrollPanel> |       </ScrollPanel> | ||||||
|       <Timeline v-else :value="dialogContent"> |       <Timeline v-else :value="dialogContent"> | ||||||
|         <template #opposite="slotProps"> |         <template #opposite="slotProps"> | ||||||
|           <small class="text-surface-500 dark:text-surface-400">{{ useTimeAgo(Date.parse(slotProps.item[0])) }}</small> |           <small class="text-surface-500 dark:text-surface-400">{{ useTimeAgo(Date.parse(slotProps.item.time)) | ||||||
|  |             }}</small> | ||||||
|         </template> |         </template> | ||||||
|         <template #content="slotProps"> |         <template #content="slotProps"> | ||||||
|           <HumanEvent :event="slotProps.item[1]" /> |           <HumanEvent :event="slotProps.item.event" /> | ||||||
|         </template> |         </template> | ||||||
|       </Timeline> |       </Timeline> | ||||||
|     </Dialog> |     </Dialog> | ||||||
| @@ -339,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> | ||||||
| @@ -353,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> | ||||||
| @@ -367,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> | ||||||
| @@ -379,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> | ||||||
| @@ -392,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"> | ||||||
| @@ -418,10 +391,8 @@ function showEventLogs() { | |||||||
|             <Column :field="ipFormat" :header="t('virtual_ipv4')" /> |             <Column :field="ipFormat" :header="t('virtual_ipv4')" /> | ||||||
|             <Column :header="t('hostname')"> |             <Column :header="t('hostname')"> | ||||||
|               <template #body="slotProps"> |               <template #body="slotProps"> | ||||||
|                 <div |                 <div v-if="!slotProps.data.route.cost || !slotProps.data.route.feature_flag.is_public_server" | ||||||
|                   v-if="!slotProps.data.route.cost || !slotProps.data.route.feature_flag.is_public_server" |                   v-tooltip="slotProps.data.route.hostname"> | ||||||
|                   v-tooltip="slotProps.data.route.hostname" |  | ||||||
|                 > |  | ||||||
|                   {{ |                   {{ | ||||||
|                     slotProps.data.route.hostname }} |                     slotProps.data.route.hostname }} | ||||||
|                 </div> |                 </div> | ||||||
| @@ -429,7 +400,7 @@ function showEventLogs() { | |||||||
|                   <Tag v-if="slotProps.data.route.feature_flag.is_public_server" severity="info" value="Info"> |                   <Tag v-if="slotProps.data.route.feature_flag.is_public_server" severity="info" value="Info"> | ||||||
|                     {{ t('status.server') }} |                     {{ t('status.server') }} | ||||||
|                   </Tag> |                   </Tag> | ||||||
|                   <Tag v-if="slotProps.data.route.no_relay_data" severity="warn" value="Warn"> |                   <Tag v-if="slotProps.data.route.feature_flag.avoid_relay_data" severity="warn" value="Warn"> | ||||||
|                     {{ t('status.relay') }} |                     {{ t('status.relay') }} | ||||||
|                   </Tag> |                   </Tag> | ||||||
|                 </div> |                 </div> | ||||||
							
								
								
									
										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 | ||||||
							
								
								
									
										198
									
								
								easytier-web/frontend-lib/src/modules/api.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								easytier-web/frontend-lib/src/modules/api.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,198 @@ | |||||||
|  | import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios'; | ||||||
|  | import { Md5 } from 'ts-md5' | ||||||
|  | import { UUID } from './utils'; | ||||||
|  |  | ||||||
|  | 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 interface ListNetworkInstanceIdResponse { | ||||||
|  |     running_inst_ids: Array<UUID>, | ||||||
|  |     disabled_inst_ids: Array<UUID>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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 list_deivce_instance_ids(machine_id: string): Promise<ListNetworkInstanceIdResponse> { | ||||||
|  |         const response = await this.client.get<any, ListNetworkInstanceIdResponse>('/machines/' + machine_id + '/networks'); | ||||||
|  |         return response; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async update_device_instance_state(machine_id: string, inst_id: string, disabled: boolean): Promise<undefined> { | ||||||
|  |         await this.client.put<string>('/machines/' + machine_id + '/networks/' + inst_id, { | ||||||
|  |             disabled: disabled, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     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,7 +11,7 @@ export interface NetworkConfig { | |||||||
| 
 | 
 | ||||||
|   dhcp: boolean |   dhcp: boolean | ||||||
|   virtual_ipv4: string |   virtual_ipv4: string | ||||||
|   network_length: number, |   network_length: number | ||||||
|   hostname?: string |   hostname?: string | ||||||
|   network_name: string |   network_name: string | ||||||
|   network_secret: string |   network_secret: string | ||||||
| @@ -84,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[] | ||||||
| @@ -97,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 | ||||||
| @@ -104,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: { | ||||||
| @@ -127,7 +135,7 @@ export interface NodeInfo { | |||||||
|     }[] |     }[] | ||||||
|   } |   } | ||||||
|   stun_info: StunInfo |   stun_info: StunInfo | ||||||
|   listeners: string[] |   listeners: Url[] | ||||||
|   vpn_portal_cfg?: string |   vpn_portal_cfg?: string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @@ -139,10 +147,7 @@ export interface StunInfo { | |||||||
| 
 | 
 | ||||||
| export interface Route { | export interface Route { | ||||||
|   peer_id: number |   peer_id: number | ||||||
|   ipv4_addr: { |   ipv4_addr: Ipv4Inet | string | null | ||||||
|     address: Ipv4Addr |  | ||||||
|     network_length: number |  | ||||||
|   } | string | null |  | ||||||
|   next_hop_peer_id: number |   next_hop_peer_id: number | ||||||
|   cost: number |   cost: number | ||||||
|   proxy_cidrs: string[] |   proxy_cidrs: string[] | ||||||
							
								
								
									
										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> | ||||||
							
								
								
									
										275
									
								
								easytier-web/frontend/src/components/DeviceManagement.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										275
									
								
								easytier-web/frontend/src/components/DeviceManagement.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,275 @@ | |||||||
|  | <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 { watch, 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 listInstanceIdResponse = ref<Api.ListNetworkInstanceIdResponse | undefined>(undefined); | ||||||
|  |  | ||||||
|  | const instanceIdList = computed(() => { | ||||||
|  |     let insts = new Set(deviceInfo.value?.running_network_instances || []); | ||||||
|  |     let t = listInstanceIdResponse.value; | ||||||
|  |     if (t) { | ||||||
|  |         t.running_inst_ids.forEach((u) => insts.add(Utils.UuidToStr(u))); | ||||||
|  |         t.disabled_inst_ids.forEach((u) => insts.add(Utils.UuidToStr(u))); | ||||||
|  |     } | ||||||
|  |     let options = Array.from(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 needShowNetworkStatus = computed(() => { | ||||||
|  |     if (!selectedInstanceId.value) { | ||||||
|  |         // nothing selected | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |     if (networkIsDisabled.value) { | ||||||
|  |         // network is disabled | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |     return true; | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | const networkIsDisabled = computed(() => { | ||||||
|  |     if (!selectedInstanceId.value) { | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |     return listInstanceIdResponse.value?.disabled_inst_ids.map(Utils.UuidToStr).includes(selectedInstanceId.value?.uuid); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | watch(selectedInstanceId, async (newVal, oldVal) => { | ||||||
|  |     if (newVal?.uuid !== oldVal?.uuid && networkIsDisabled.value) { | ||||||
|  |         await loadDisabledNetworkConfig(); | ||||||
|  |     } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | const disabledNetworkConfig = ref<NetworkTypes.NetworkConfig | undefined>(undefined); | ||||||
|  |  | ||||||
|  | const loadDisabledNetworkConfig = async () => { | ||||||
|  |     disabledNetworkConfig.value = undefined; | ||||||
|  |  | ||||||
|  |     if (!deviceId.value || !selectedInstanceId.value) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let ret = await props.api?.get_network_config(deviceId.value, selectedInstanceId.value.uuid); | ||||||
|  |     disabledNetworkConfig.value = ret; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const updateNetworkState = async (disabled: boolean) => { | ||||||
|  |     if (!deviceId.value || !selectedInstanceId.value) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     await props.api?.update_device_instance_state(deviceId.value, selectedInstanceId.value.uuid, disabled); | ||||||
|  |     await loadNetworkInstanceIds(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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 loadNetworkInstanceIds = async () => { | ||||||
|  |     if (!deviceId.value) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     listInstanceIdResponse.value = await props.api?.list_deivce_instance_ids(deviceId.value); | ||||||
|  |     console.debug("loadNetworkInstanceIds", listInstanceIdResponse.value); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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 Promise.all([loadNetworkInstanceIds(), 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> | ||||||
|  |  | ||||||
|  |     <!-- For running network, show the status --> | ||||||
|  |     <div v-if="needShowNetworkStatus"> | ||||||
|  |         <Status v-bind:cur-network-inst="curNetworkInfo" v-if="needShowNetworkStatus"> | ||||||
|  |         </Status> | ||||||
|  |         <center> | ||||||
|  |             <Button @click="updateNetworkState(true)" label="Disable Network" severity="warn" /> | ||||||
|  |         </center> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <!-- For disabled network, show the config --> | ||||||
|  |     <div v-if="networkIsDisabled"> | ||||||
|  |         <Config :cur-network="disabledNetworkConfig" @run-network="updateNetworkState(false)" | ||||||
|  |             v-if="disabledNetworkConfig" /> | ||||||
|  |         <div v-else> | ||||||
|  |             <div class="text-center text-xl"> Network is disabled, Loading config... </div> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <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> | ||||||
							
								
								
									
										194
									
								
								easytier-web/frontend/src/components/Login.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										194
									
								
								easytier-web/frontend/src/components/Login.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,194 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { computed, onMounted, 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()); | ||||||
|  |  | ||||||
|  | interface ApiHost { | ||||||
|  |     value: string; | ||||||
|  |     usedAt: number; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const isValidHttpUrl = (s: string): boolean => { | ||||||
|  |     let url; | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |         url = new URL(s); | ||||||
|  |     } catch (_) { | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return url.protocol === "http:" || url.protocol === "https:"; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const cleanAndLoadApiHosts = (): Array<ApiHost> => { | ||||||
|  |     const maxHosts = 10; | ||||||
|  |     const apiHosts = localStorage.getItem('apiHosts'); | ||||||
|  |     if (apiHosts) { | ||||||
|  |         const hosts: Array<ApiHost> = JSON.parse(apiHosts); | ||||||
|  |         // sort by usedAt | ||||||
|  |         hosts.sort((a, b) => b.usedAt - a.usedAt); | ||||||
|  |  | ||||||
|  |         // only keep the first 10 | ||||||
|  |         if (hosts.length > maxHosts) { | ||||||
|  |             hosts.splice(maxHosts); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         localStorage.setItem('apiHosts', JSON.stringify(hosts)); | ||||||
|  |         return hosts; | ||||||
|  |     } else { | ||||||
|  |         return []; | ||||||
|  |     } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const saveApiHost = (host: string) => { | ||||||
|  |     console.log('Save API Host:', host); | ||||||
|  |     if (!isValidHttpUrl(host)) { | ||||||
|  |         console.error('Invalid API Host:', host); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let hosts = cleanAndLoadApiHosts(); | ||||||
|  |     const newHost: ApiHost = { value: host, usedAt: Date.now() }; | ||||||
|  |     hosts = hosts.filter((h) => h.value !== host); | ||||||
|  |     hosts.push(newHost); | ||||||
|  |     localStorage.setItem('apiHosts', JSON.stringify(hosts)); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const onSubmit = async () => { | ||||||
|  |     // Add your login logic here | ||||||
|  |     saveApiHost(apiHost.value); | ||||||
|  |     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 () => { | ||||||
|  |     saveApiHost(apiHost.value); | ||||||
|  |     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 getInitialApiHost = (): string => { | ||||||
|  |     const hosts = cleanAndLoadApiHosts(); | ||||||
|  |     if (hosts.length > 0) { | ||||||
|  |         return hosts[0].value; | ||||||
|  |     } else { | ||||||
|  |         return defaultApiHost; | ||||||
|  |     } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const defaultApiHost = 'https://config-server.easytier.cn' | ||||||
|  | const apiHost = ref<string>(getInitialApiHost()) | ||||||
|  | const apiHostSuggestions = ref<Array<string>>([]) | ||||||
|  | const apiHostSearch = async (event: { query: string }) => { | ||||||
|  |     apiHostSuggestions.value = []; | ||||||
|  |     let hosts = cleanAndLoadApiHosts(); | ||||||
|  |     if (event.query) { | ||||||
|  |         apiHostSuggestions.value.push(event.query); | ||||||
|  |     } | ||||||
|  |     hosts.forEach((host) => { | ||||||
|  |         apiHostSuggestions.value.push(host.value); | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | onMounted(() => { | ||||||
|  |     let hosts = cleanAndLoadApiHosts(); | ||||||
|  |     if (hosts.length === 0) { | ||||||
|  |         saveApiHost(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="saveApiHost(apiHost); $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="saveApiHost(apiHost); $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()], | ||||||
|  | }) | ||||||
							
								
								
									
										24
									
								
								easytier-web/locales/app.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								easytier-web/locales/app.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | _version: 2 | ||||||
|  |  | ||||||
|  | cli: | ||||||
|  |   db: | ||||||
|  |     en: "path to the sqlite3 database file, used to save all the data" | ||||||
|  |     zh-CN: "sqlite3 数据库文件路径, 用于保存所有数据" | ||||||
|  |   console_log_level: | ||||||
|  |     en: "The log level for the console logger. Possible values: trace, debug, info, warn, error" | ||||||
|  |     zh-CN: "控制台日志级别。可能的值:trace, debug, info, warn, error" | ||||||
|  |   file_log_level: | ||||||
|  |     en: "The log level for the file logger. Possible values: trace, debug, info, warn, error" | ||||||
|  |     zh-CN: "文件日志级别。可能的值:trace, debug, info, warn, error" | ||||||
|  |   file_log_dir: | ||||||
|  |     en: "The directory to save the log files, default is the current directory" | ||||||
|  |     zh-CN: "保存日志文件的目录,默认为当前目录" | ||||||
|  |   config_server_port: | ||||||
|  |     en: "The port to listen for the config server, used by the easytier-core to connect to" | ||||||
|  |     zh-CN: "配置服务器的监听端口,用于被 easytier-core 连接" | ||||||
|  |   config_server_protocol: | ||||||
|  |     en: "The protocol to listen for the config server, used by the easytier-core to connect to" | ||||||
|  |     zh-CN: "配置服务器的监听协议,用于被 easytier-core 连接, 可能的值:udp, tcp" | ||||||
|  |   api_server_port: | ||||||
|  |     en: "The port to listen for the restful server, acting as ApiHost and used by the web frontend" | ||||||
|  |     zh-CN: "restful 服务器的监听端口,作为 ApiHost 并被 web 前端使用" | ||||||
							
								
								
									
										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 async 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); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										280
									
								
								easytier-web/src/client_manager/session.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										280
									
								
								easytier-web/src/client_manager/session.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,280 @@ | |||||||
|  | use std::{fmt::Debug, str::FromStr as _, 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 crate::db::ListNetworkProps; | ||||||
|  |  | ||||||
|  | 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()) { | ||||||
|  |             let Ok(report_time) = chrono::DateTime::<chrono::Local>::from_str(&req.report_time) | ||||||
|  |             else { | ||||||
|  |                 tracing::error!("Failed to parse report time: {:?}", req.report_time); | ||||||
|  |                 return Ok(HeartbeatResponse {}); | ||||||
|  |             }; | ||||||
|  |             storage.update_client( | ||||||
|  |                 data.storage_token.as_ref().unwrap().clone(), | ||||||
|  |                 report_time.timestamp(), | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         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()), | ||||||
|  |                     ListNetworkProps::EnabledOnly, | ||||||
|  |                 ) | ||||||
|  |                 .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() | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										132
									
								
								easytier-web/src/client_manager/storage.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								easytier-web/src/client_manager/storage.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,132 @@ | |||||||
|  | use std::sync::{Arc, Weak}; | ||||||
|  |  | ||||||
|  | use dashmap::DashMap; | ||||||
|  |  | ||||||
|  | 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, Clone)] | ||||||
|  | struct ClientInfo { | ||||||
|  |     client_url: url::Url, | ||||||
|  |     machine_id: uuid::Uuid, | ||||||
|  |     token: String, | ||||||
|  |     report_time: i64, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug)] | ||||||
|  | pub struct StorageInner { | ||||||
|  |     // some map for indexing | ||||||
|  |     token_clients_map: DashMap<String, DashMap<uuid::Uuid, ClientInfo>>, | ||||||
|  |     machine_client_url_map: DashMap<uuid::Uuid, ClientInfo>, | ||||||
|  |     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, | ||||||
|  |         })) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn remove_mid_to_client_info_map( | ||||||
|  |         map: &DashMap<uuid::Uuid, ClientInfo>, | ||||||
|  |         machine_id: &uuid::Uuid, | ||||||
|  |         client_url: &url::Url, | ||||||
|  |     ) { | ||||||
|  |         map.remove_if(&machine_id, |_, v| v.client_url == *client_url); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn update_mid_to_client_info_map( | ||||||
|  |         map: &DashMap<uuid::Uuid, ClientInfo>, | ||||||
|  |         client_info: &ClientInfo, | ||||||
|  |     ) { | ||||||
|  |         map.entry(client_info.machine_id) | ||||||
|  |             .and_modify(|e| { | ||||||
|  |                 if e.report_time < client_info.report_time { | ||||||
|  |                     assert_eq!(e.machine_id, client_info.machine_id); | ||||||
|  |                     *e = client_info.clone(); | ||||||
|  |                 } | ||||||
|  |             }) | ||||||
|  |             .or_insert(client_info.clone()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub fn update_client(&self, stoken: StorageToken, report_time: i64) { | ||||||
|  |         let inner = self | ||||||
|  |             .0 | ||||||
|  |             .token_clients_map | ||||||
|  |             .entry(stoken.token.clone()) | ||||||
|  |             .or_insert_with(DashMap::new); | ||||||
|  |  | ||||||
|  |         let client_info = ClientInfo { | ||||||
|  |             client_url: stoken.client_url.clone(), | ||||||
|  |             machine_id: stoken.machine_id, | ||||||
|  |             token: stoken.token.clone(), | ||||||
|  |             report_time, | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         Self::update_mid_to_client_info_map(&inner, &client_info); | ||||||
|  |         Self::update_mid_to_client_info_map(&self.0.machine_client_url_map, &client_info); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub fn remove_client(&self, stoken: &StorageToken) { | ||||||
|  |         self.0.token_clients_map.remove_if(&stoken.token, |_, set| { | ||||||
|  |             Self::remove_mid_to_client_info_map(set, &stoken.machine_id, &stoken.client_url); | ||||||
|  |             set.is_empty() | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         Self::remove_mid_to_client_info_map( | ||||||
|  |             &self.0.machine_client_url_map, | ||||||
|  |             &stoken.machine_id, | ||||||
|  |             &stoken.client_url, | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     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(|info| info.client_url.clone()) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub fn list_token_clients(&self, token: &str) -> Vec<url::Url> { | ||||||
|  |         self.0 | ||||||
|  |             .token_clients_map | ||||||
|  |             .get(token) | ||||||
|  |             .map(|info_map| { | ||||||
|  |                 info_map | ||||||
|  |                     .iter() | ||||||
|  |                     .map(|info| info.value().client_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 {} | ||||||
							
								
								
									
										285
									
								
								easytier-web/src/db/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										285
									
								
								easytier-web/src/db/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,285 @@ | |||||||
|  | // 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::{ | ||||||
|  |     prelude::Expr, sea_query::OnConflict, ColumnTrait as _, DatabaseConnection, DbErr, EntityTrait, | ||||||
|  |     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; | ||||||
|  |  | ||||||
|  | pub enum ListNetworkProps { | ||||||
|  |     All, | ||||||
|  |     EnabledOnly, | ||||||
|  |     DisabledOnly, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[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 update_network_config_state( | ||||||
|  |         &self, | ||||||
|  |         user_id: UserIdInDb, | ||||||
|  |         network_inst_id: uuid::Uuid, | ||||||
|  |         disabled: bool, | ||||||
|  |     ) -> Result<entity::user_running_network_configs::Model, DbErr> { | ||||||
|  |         use entity::user_running_network_configs as urnc; | ||||||
|  |  | ||||||
|  |         urnc::Entity::update_many() | ||||||
|  |             .filter(urnc::Column::UserId.eq(user_id)) | ||||||
|  |             .filter(urnc::Column::NetworkInstanceId.eq(network_inst_id.to_string())) | ||||||
|  |             .col_expr(urnc::Column::Disabled, Expr::value(disabled)) | ||||||
|  |             .col_expr( | ||||||
|  |                 urnc::Column::UpdateTime, | ||||||
|  |                 Expr::value(chrono::Local::now().fixed_offset()), | ||||||
|  |             ) | ||||||
|  |             .exec(self.orm_db()) | ||||||
|  |             .await?; | ||||||
|  |  | ||||||
|  |         urnc::Entity::find() | ||||||
|  |             .filter(urnc::Column::UserId.eq(user_id)) | ||||||
|  |             .filter(urnc::Column::NetworkInstanceId.eq(network_inst_id.to_string())) | ||||||
|  |             .one(self.orm_db()) | ||||||
|  |             .await? | ||||||
|  |             .ok_or(DbErr::RecordNotFound(format!( | ||||||
|  |                 "Network config not found for user {} and network instance {}", | ||||||
|  |                 user_id, network_inst_id | ||||||
|  |             ))) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub async fn list_network_configs( | ||||||
|  |         &self, | ||||||
|  |         user_id: UserIdInDb, | ||||||
|  |         device_id: Option<uuid::Uuid>, | ||||||
|  |         props: ListNetworkProps, | ||||||
|  |     ) -> 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 matches!( | ||||||
|  |             props, | ||||||
|  |             ListNetworkProps::EnabledOnly | ListNetworkProps::DisabledOnly | ||||||
|  |         ) { | ||||||
|  |             configs | ||||||
|  |                 .filter(urnc::Column::Disabled.eq(matches!(props, ListNetworkProps::DisabledOnly))) | ||||||
|  |         } 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_network_config( | ||||||
|  |         &self, | ||||||
|  |         user_id: UserIdInDb, | ||||||
|  |         device_id: &uuid::Uuid, | ||||||
|  |         network_inst_id: &String, | ||||||
|  |     ) -> Result<Option<user_running_network_configs::Model>, DbErr> { | ||||||
|  |         use entity::user_running_network_configs as urnc; | ||||||
|  |  | ||||||
|  |         let config = urnc::Entity::find() | ||||||
|  |             .filter(urnc::Column::UserId.eq(user_id)) | ||||||
|  |             .filter(urnc::Column::DeviceId.eq(device_id.to_string())) | ||||||
|  |             .filter(urnc::Column::NetworkInstanceId.eq(network_inst_id)) | ||||||
|  |             .one(self.orm_db()) | ||||||
|  |             .await?; | ||||||
|  |  | ||||||
|  |         Ok(config) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     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, ListNetworkProps}; | ||||||
|  |  | ||||||
|  |     #[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), ListNetworkProps::All) | ||||||
|  |                 .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()); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										116
									
								
								easytier-web/src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								easytier-web/src/main.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | |||||||
|  | #![allow(dead_code)] | ||||||
|  |  | ||||||
|  | #[macro_use] | ||||||
|  | extern crate rust_i18n; | ||||||
|  |  | ||||||
|  | use std::sync::Arc; | ||||||
|  |  | ||||||
|  | use clap::{command, Parser}; | ||||||
|  | use easytier::{ | ||||||
|  |     common::{ | ||||||
|  |         config::{ConfigLoader, ConsoleLoggerConfig, FileLoggerConfig, TomlConfigLoader}, | ||||||
|  |         constants::EASYTIER_VERSION, | ||||||
|  |     }, | ||||||
|  |     tunnel::udp::UdpTunnelListener, | ||||||
|  |     utils::{init_logger, setup_panic_handler}, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | mod client_manager; | ||||||
|  | mod db; | ||||||
|  | mod migrator; | ||||||
|  | mod restful; | ||||||
|  |  | ||||||
|  | rust_i18n::i18n!("locales", fallback = "en"); | ||||||
|  |  | ||||||
|  | #[derive(Parser, Debug)] | ||||||
|  | #[command(name = "easytier-core", author, version = EASYTIER_VERSION , about, long_about = None)] | ||||||
|  | struct Cli { | ||||||
|  |     #[arg(short, long, default_value = "et.db", help = t!("cli.db").to_string())] | ||||||
|  |     db: String, | ||||||
|  |  | ||||||
|  |     #[arg( | ||||||
|  |         long, | ||||||
|  |         help = t!("cli.console_log_level").to_string(), | ||||||
|  |     )] | ||||||
|  |     console_log_level: Option<String>, | ||||||
|  |  | ||||||
|  |     #[arg( | ||||||
|  |         long, | ||||||
|  |         help = t!("cli.file_log_level").to_string(), | ||||||
|  |     )] | ||||||
|  |     file_log_level: Option<String>, | ||||||
|  |  | ||||||
|  |     #[arg( | ||||||
|  |         long, | ||||||
|  |         help = t!("cli.file_log_dir").to_string(), | ||||||
|  |     )] | ||||||
|  |     file_log_dir: Option<String>, | ||||||
|  |  | ||||||
|  |     #[arg( | ||||||
|  |         long, | ||||||
|  |         short='c', | ||||||
|  |         default_value = "22020", | ||||||
|  |         help = t!("cli.config_server_port").to_string(), | ||||||
|  |     )] | ||||||
|  |     config_server_port: u16, | ||||||
|  |  | ||||||
|  |     #[arg( | ||||||
|  |         long, | ||||||
|  |         short='p', | ||||||
|  |         default_value = "udp", | ||||||
|  |         help = t!("cli.config_server_protocol").to_string(), | ||||||
|  |     )] | ||||||
|  |     config_server_protocol: String, | ||||||
|  |  | ||||||
|  |     #[arg( | ||||||
|  |         long, | ||||||
|  |         short='a', | ||||||
|  |         default_value = "11211", | ||||||
|  |         help = t!("cli.api_server_port").to_string(), | ||||||
|  |     )] | ||||||
|  |     api_server_port: u16, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[tokio::main] | ||||||
|  | async fn main() { | ||||||
|  |     let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US")); | ||||||
|  |     rust_i18n::set_locale(&locale); | ||||||
|  |     setup_panic_handler(); | ||||||
|  |  | ||||||
|  |     let cli = Cli::parse(); | ||||||
|  |     let config = TomlConfigLoader::default(); | ||||||
|  |     config.set_console_logger_config(ConsoleLoggerConfig { | ||||||
|  |         level: cli.console_log_level, | ||||||
|  |     }); | ||||||
|  |     config.set_file_logger_config(FileLoggerConfig { | ||||||
|  |         dir: cli.file_log_dir, | ||||||
|  |         level: cli.file_log_level, | ||||||
|  |         file: None, | ||||||
|  |     }); | ||||||
|  |     init_logger(config, false).unwrap(); | ||||||
|  |  | ||||||
|  |     // let db = db::Db::new(":memory:").await.unwrap(); | ||||||
|  |     let db = db::Db::new(cli.db).await.unwrap(); | ||||||
|  |  | ||||||
|  |     let listener = UdpTunnelListener::new( | ||||||
|  |         format!( | ||||||
|  |             "{}://0.0.0.0:{}", | ||||||
|  |             cli.config_server_protocol, cli.config_server_port | ||||||
|  |         ) | ||||||
|  |         .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( | ||||||
|  |         format!("0.0.0.0:{}", cli.api_server_port).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(()) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								easytier-web/src/migrator/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								easytier-web/src/migrator/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | use sea_orm_migration::prelude::*; | ||||||
|  |  | ||||||
|  | mod m20241029_000001_init; | ||||||
|  |  | ||||||
|  | pub struct Migrator; | ||||||
|  |  | ||||||
|  | #[async_trait::async_trait] | ||||||
|  | impl MigratorTrait for Migrator { | ||||||
|  |     fn migrations() -> Vec<Box<dyn MigrationTrait>> { | ||||||
|  |         vec![Box::new(m20241029_000001_init::Migration)] | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										188
									
								
								easytier-web/src/restful/auth.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								easytier-web/src/restful/auth.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,188 @@ | |||||||
|  | use axum::{ | ||||||
|  |     http::StatusCode, | ||||||
|  |     routing::{get, post, put}, | ||||||
|  |     Router, | ||||||
|  | }; | ||||||
|  | use axum_login::login_required; | ||||||
|  | use axum_messages::Message; | ||||||
|  | use serde::{Deserialize, Serialize}; | ||||||
|  |  | ||||||
|  | use crate::restful::users::Backend; | ||||||
|  |  | ||||||
|  | use super::{ | ||||||
|  |     users::{AuthSession, Credentials}, | ||||||
|  |     AppStateInner, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | #[derive(Debug, Deserialize, Serialize)] | ||||||
|  | pub struct LoginResult { | ||||||
|  |     messages: Vec<Message>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn router() -> Router<AppStateInner> { | ||||||
|  |     let r = Router::new() | ||||||
|  |         .route("/api/v1/auth/password", put(self::put::change_password)) | ||||||
|  |         .route( | ||||||
|  |             "/api/v1/auth/check_login_status", | ||||||
|  |             get(self::get::check_login_status), | ||||||
|  |         ) | ||||||
|  |         .route_layer(login_required!(Backend)); | ||||||
|  |     Router::new() | ||||||
|  |         .merge(r) | ||||||
|  |         .route("/api/v1/auth/login", post(self::post::login)) | ||||||
|  |         .route("/api/v1/auth/logout", get(self::get::logout)) | ||||||
|  |         .route("/api/v1/auth/captcha", get(self::get::get_captcha)) | ||||||
|  |         .route("/api/v1/auth/register", post(self::post::register)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | mod put { | ||||||
|  |     use axum::Json; | ||||||
|  |     use axum_login::AuthUser; | ||||||
|  |     use easytier::proto::common::Void; | ||||||
|  |  | ||||||
|  |     use crate::restful::{other_error, users::ChangePassword, HttpHandleError}; | ||||||
|  |  | ||||||
|  |     use super::*; | ||||||
|  |  | ||||||
|  |     pub async fn change_password( | ||||||
|  |         mut auth_session: AuthSession, | ||||||
|  |         Json(req): Json<ChangePassword>, | ||||||
|  |     ) -> Result<Json<Void>, HttpHandleError> { | ||||||
|  |         if let Err(e) = auth_session | ||||||
|  |             .backend | ||||||
|  |             .change_password(auth_session.user.as_ref().unwrap().id(), &req) | ||||||
|  |             .await | ||||||
|  |         { | ||||||
|  |             tracing::error!("Failed to change password: {:?}", e); | ||||||
|  |             return Err(( | ||||||
|  |                 StatusCode::INTERNAL_SERVER_ERROR, | ||||||
|  |                 Json::from(other_error(format!("{:?}", e))), | ||||||
|  |             )); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         let _ = auth_session.logout().await; | ||||||
|  |  | ||||||
|  |         Ok(Void::default().into()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | mod post { | ||||||
|  |     use axum::Json; | ||||||
|  |     use easytier::proto::common::Void; | ||||||
|  |  | ||||||
|  |     use crate::restful::{ | ||||||
|  |         captcha::extension::{axum_tower_sessions::CaptchaAxumTowerSessionStaticExt, CaptchaUtil}, | ||||||
|  |         other_error, | ||||||
|  |         users::RegisterNewUser, | ||||||
|  |         HttpHandleError, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     use super::*; | ||||||
|  |  | ||||||
|  |     pub async fn login( | ||||||
|  |         mut auth_session: AuthSession, | ||||||
|  |         Json(creds): Json<Credentials>, | ||||||
|  |     ) -> Result<Json<Void>, HttpHandleError> { | ||||||
|  |         let user = match auth_session.authenticate(creds.clone()).await { | ||||||
|  |             Ok(Some(user)) => user, | ||||||
|  |             Ok(None) => { | ||||||
|  |                 return Err(( | ||||||
|  |                     StatusCode::UNAUTHORIZED, | ||||||
|  |                     Json::from(other_error("Invalid credentials")), | ||||||
|  |                 )); | ||||||
|  |             } | ||||||
|  |             Err(e) => { | ||||||
|  |                 return Err(( | ||||||
|  |                     StatusCode::INTERNAL_SERVER_ERROR, | ||||||
|  |                     Json::from(other_error(format!("{:?}", e))), | ||||||
|  |                 )) | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         if let Err(e) = auth_session.login(&user).await { | ||||||
|  |             return Err(( | ||||||
|  |                 StatusCode::INTERNAL_SERVER_ERROR, | ||||||
|  |                 Json::from(other_error(format!("{:?}", e))), | ||||||
|  |             )); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         Ok(Void::default().into()) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub async fn register( | ||||||
|  |         auth_session: AuthSession, | ||||||
|  |         captcha_session: tower_sessions::Session, | ||||||
|  |         Json(req): Json<RegisterNewUser>, | ||||||
|  |     ) -> Result<Json<Void>, HttpHandleError> { | ||||||
|  |         // 调用CaptchaUtil的静态方法验证验证码是否正确 | ||||||
|  |         if !CaptchaUtil::ver(&req.captcha, &captcha_session).await { | ||||||
|  |             return Err(( | ||||||
|  |                 StatusCode::BAD_REQUEST, | ||||||
|  |                 other_error(format!("captcha verify error, input: {}", req.captcha)).into(), | ||||||
|  |             )); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if let Err(e) = auth_session.backend.register_new_user(&req).await { | ||||||
|  |             tracing::error!("Failed to register new user: {:?}", e); | ||||||
|  |             return Err(( | ||||||
|  |                 StatusCode::BAD_REQUEST, | ||||||
|  |                 other_error(format!("{:?}", e)).into(), | ||||||
|  |             )); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         Ok(Void::default().into()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | mod get { | ||||||
|  |     use crate::restful::{ | ||||||
|  |         captcha::{ | ||||||
|  |             captcha::spec::SpecCaptcha, | ||||||
|  |             extension::{axum_tower_sessions::CaptchaAxumTowerSessionExt as _, CaptchaUtil}, | ||||||
|  |             NewCaptcha as _, | ||||||
|  |         }, | ||||||
|  |         other_error, HttpHandleError, | ||||||
|  |     }; | ||||||
|  |     use axum::{response::Response, Json}; | ||||||
|  |     use easytier::proto::common::Void; | ||||||
|  |     use tower_sessions::Session; | ||||||
|  |  | ||||||
|  |     use super::*; | ||||||
|  |  | ||||||
|  |     pub async fn logout(mut auth_session: AuthSession) -> Result<Json<Void>, HttpHandleError> { | ||||||
|  |         match auth_session.logout().await { | ||||||
|  |             Ok(_) => Ok(Json(Void::default())), | ||||||
|  |             Err(e) => { | ||||||
|  |                 tracing::error!("Failed to logout: {:?}", e); | ||||||
|  |                 Err(( | ||||||
|  |                     StatusCode::INTERNAL_SERVER_ERROR, | ||||||
|  |                     Json::from(other_error(format!("{:?}", e))), | ||||||
|  |                 )) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub async fn get_captcha(session: Session) -> Result<Response, HttpHandleError> { | ||||||
|  |         let mut captcha: CaptchaUtil<SpecCaptcha> = CaptchaUtil::with_size_and_len(127, 48, 4); | ||||||
|  |         match captcha.out(&session).await { | ||||||
|  |             Ok(response) => Ok(response), | ||||||
|  |             Err(e) => Err(( | ||||||
|  |                 StatusCode::INTERNAL_SERVER_ERROR, | ||||||
|  |                 Json::from(other_error(format!("{:?}", e))), | ||||||
|  |             )), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub async fn check_login_status( | ||||||
|  |         auth_session: AuthSession, | ||||||
|  |     ) -> Result<Json<Void>, HttpHandleError> { | ||||||
|  |         if auth_session.user.is_some() { | ||||||
|  |             Ok(Json(Void::default())) | ||||||
|  |         } else { | ||||||
|  |             Err(( | ||||||
|  |                 StatusCode::UNAUTHORIZED, | ||||||
|  |                 Json::from(other_error("Not logged in")), | ||||||
|  |             )) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user