mirror of
https://github.com/fatedier/frp.git
synced 2026-07-31 07:22:54 +08:00
Compare commits
23
Commits
04be5e9a75
...
v0.68.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ec8de973f | ||
|
|
e8dfd6efcc | ||
|
|
a9a4416ecf | ||
|
|
d667be7a0a | ||
|
|
31c3deb4f7 | ||
|
|
31e271939b | ||
|
|
061c141756 | ||
|
|
98ee1adb13 | ||
|
|
76abeff881 | ||
|
|
c694b1f6a9 | ||
|
|
5bfcea3d0c | ||
|
|
5ed02275da | ||
|
|
60c4f5d4bd | ||
|
|
0a1b4ab21f | ||
|
|
d20e384bf1 | ||
|
|
c95dc9d88a | ||
|
|
38a71a6803 | ||
|
|
6cdef90113 | ||
|
|
5f575b8442 | ||
|
|
a1348cdf00 | ||
|
|
2f5e1f7945 | ||
|
|
22ae8166d3 | ||
|
|
af6bc6369d |
@@ -19,15 +19,15 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# environment
|
# environment
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: '0'
|
fetch-depth: '0'
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v4
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
# get image tag name
|
# get image tag name
|
||||||
- name: Get Image Tag Name
|
- name: Get Image Tag Name
|
||||||
@@ -38,13 +38,13 @@ jobs:
|
|||||||
echo "TAG_NAME=${{ github.event.inputs.tag }}" >> $GITHUB_ENV
|
echo "TAG_NAME=${{ github.event.inputs.tag }}" >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||||
|
|
||||||
- name: Login to the GPR
|
- name: Login to the GPR
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
echo "TAG_FRPS_GPR=ghcr.io/fatedier/frps:${{ env.TAG_NAME }}" >> $GITHUB_ENV
|
echo "TAG_FRPS_GPR=ghcr.io/fatedier/frps:${{ env.TAG_NAME }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Build and push frpc
|
- name: Build and push frpc
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./dockerfiles/Dockerfile-for-frpc
|
file: ./dockerfiles/Dockerfile-for-frpc
|
||||||
@@ -72,7 +72,7 @@ jobs:
|
|||||||
${{ env.TAG_FRPC_GPR }}
|
${{ env.TAG_FRPC_GPR }}
|
||||||
|
|
||||||
- name: Build and push frps
|
- name: Build and push frps
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./dockerfiles/Dockerfile-for-frps
|
file: ./dockerfiles/Dockerfile-for-frps
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ jobs:
|
|||||||
name: lint
|
name: lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.25'
|
go-version: '1.25'
|
||||||
cache: false
|
cache: false
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
- name: Build web assets (frps)
|
- name: Build web assets (frps)
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.25'
|
go-version: '1.25'
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
- name: Build web assets (frps)
|
- name: Build web assets (frps)
|
||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
./package.sh
|
./package.sh
|
||||||
|
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@v6
|
uses: goreleaser/goreleaser-action@v7
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: release --clean --release-notes=./Release.md
|
args: release --clean --release-notes=./Release.md
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
actions: write
|
actions: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v9
|
- uses: actions/stale@v10
|
||||||
with:
|
with:
|
||||||
stale-issue-message: 'Issues go stale after 14d of inactivity. Stale issues rot after an additional 3d of inactivity and eventually close.'
|
stale-issue-message: 'Issues go stale after 14d of inactivity. Stale issues rot after an additional 3d of inactivity and eventually close.'
|
||||||
stale-pr-message: "PRs go stale after 14d of inactivity. Stale PRs rot after an additional 3d of inactivity and eventually close."
|
stale-pr-message: "PRs go stale after 14d of inactivity. Stale PRs rot after an additional 3d of inactivity and eventually close."
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ dist/
|
|||||||
client.crt
|
client.crt
|
||||||
client.key
|
client.key
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
|
||||||
# Cache
|
# Cache
|
||||||
*.swp
|
*.swp
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ linters:
|
|||||||
- third_party$
|
- third_party$
|
||||||
- builtin$
|
- builtin$
|
||||||
- examples$
|
- examples$
|
||||||
|
- node_modules
|
||||||
formatters:
|
formatters:
|
||||||
enable:
|
enable:
|
||||||
- gci
|
- gci
|
||||||
@@ -112,6 +113,7 @@ formatters:
|
|||||||
- third_party$
|
- third_party$
|
||||||
- builtin$
|
- builtin$
|
||||||
- examples$
|
- examples$
|
||||||
|
- node_modules
|
||||||
issues:
|
issues:
|
||||||
max-issues-per-linter: 0
|
max-issues-per-linter: 0
|
||||||
max-same-issues: 0
|
max-same-issues: 0
|
||||||
|
|||||||
@@ -32,3 +32,8 @@
|
|||||||
- E2E tests using Ginkgo/Gomega framework
|
- E2E tests using Ginkgo/Gomega framework
|
||||||
- Mock servers in `/test/e2e/mock/`
|
- Mock servers in `/test/e2e/mock/`
|
||||||
- Run: `make e2e` or `make alltest`
|
- Run: `make e2e` or `make alltest`
|
||||||
|
|
||||||
|
## Agent Runbooks
|
||||||
|
|
||||||
|
Operational procedures for agents are in `doc/agents/`:
|
||||||
|
- `doc/agents/release.md` - Release process
|
||||||
|
|||||||
@@ -13,6 +13,16 @@ frp is an open source project with its ongoing development made possible entirel
|
|||||||
|
|
||||||
<h3 align="center">Gold Sponsors</h3>
|
<h3 align="center">Gold Sponsors</h3>
|
||||||
<!--gold sponsors start-->
|
<!--gold sponsors start-->
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/beclab/Olares" target="_blank">
|
||||||
|
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
|
||||||
|
<br>
|
||||||
|
<b>The sovereign cloud that puts you in control</b>
|
||||||
|
<br>
|
||||||
|
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
## Recall.ai - API for meeting recordings
|
## Recall.ai - API for meeting recordings
|
||||||
@@ -40,16 +50,6 @@ an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and
|
|||||||
<b>The complete IDE crafted for professional Go developers</b>
|
<b>The complete IDE crafted for professional Go developers</b>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://github.com/beclab/Olares" target="_blank">
|
|
||||||
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
|
|
||||||
<br>
|
|
||||||
<b>The sovereign cloud that puts you in control</b>
|
|
||||||
<br>
|
|
||||||
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<!--gold sponsors end-->
|
<!--gold sponsors end-->
|
||||||
|
|
||||||
## What is frp?
|
## What is frp?
|
||||||
@@ -81,6 +81,7 @@ frp also offers a P2P connect mode.
|
|||||||
* [Split Configures Into Different Files](#split-configures-into-different-files)
|
* [Split Configures Into Different Files](#split-configures-into-different-files)
|
||||||
* [Server Dashboard](#server-dashboard)
|
* [Server Dashboard](#server-dashboard)
|
||||||
* [Client Admin UI](#client-admin-ui)
|
* [Client Admin UI](#client-admin-ui)
|
||||||
|
* [Dynamic Proxy Management (Store)](#dynamic-proxy-management-store)
|
||||||
* [Monitor](#monitor)
|
* [Monitor](#monitor)
|
||||||
* [Prometheus](#prometheus)
|
* [Prometheus](#prometheus)
|
||||||
* [Authenticating the Client](#authenticating-the-client)
|
* [Authenticating the Client](#authenticating-the-client)
|
||||||
@@ -149,7 +150,9 @@ We sincerely appreciate your support for frp.
|
|||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||

|
<p align="center">
|
||||||
|
<img src="/doc/pic/architecture.jpg" alt="architecture" width="760">
|
||||||
|
</p>
|
||||||
|
|
||||||
## Example Usage
|
## Example Usage
|
||||||
|
|
||||||
@@ -593,7 +596,7 @@ Then visit `https://[serverAddr]:7500` to see the dashboard in secure HTTPS conn
|
|||||||
|
|
||||||
### Client Admin UI
|
### Client Admin UI
|
||||||
|
|
||||||
The Client Admin UI helps you check and manage frpc's configuration.
|
The Client Admin UI helps you check and manage frpc's configuration and proxies.
|
||||||
|
|
||||||
Configure an address for admin UI to enable this feature:
|
Configure an address for admin UI to enable this feature:
|
||||||
|
|
||||||
@@ -606,6 +609,19 @@ webServer.password = "admin"
|
|||||||
|
|
||||||
Then visit `http://127.0.0.1:7400` to see admin UI, with username and password both being `admin`.
|
Then visit `http://127.0.0.1:7400` to see admin UI, with username and password both being `admin`.
|
||||||
|
|
||||||
|
#### Dynamic Proxy Management (Store)
|
||||||
|
|
||||||
|
You can dynamically create, update, and delete proxies and visitors at runtime through the Web UI or API, without restarting frpc.
|
||||||
|
|
||||||
|
To enable this feature, configure `store.path` to specify a file for persisting the configurations:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[store]
|
||||||
|
path = "./db.json"
|
||||||
|
```
|
||||||
|
|
||||||
|
Proxies and visitors managed through the Store are saved to disk and automatically restored on frpc restart. They work alongside proxies defined in the configuration file — Store entries take precedence when names conflict.
|
||||||
|
|
||||||
### Monitor
|
### Monitor
|
||||||
|
|
||||||
When web server is enabled, frps will save monitor data in cache for 7 days. It will be cleared after process restart.
|
When web server is enabled, frps will save monitor data in cache for 7 days. It will be cleared after process restart.
|
||||||
|
|||||||
+10
-10
@@ -15,6 +15,16 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者
|
|||||||
|
|
||||||
<h3 align="center">Gold Sponsors</h3>
|
<h3 align="center">Gold Sponsors</h3>
|
||||||
<!--gold sponsors start-->
|
<!--gold sponsors start-->
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/beclab/Olares" target="_blank">
|
||||||
|
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
|
||||||
|
<br>
|
||||||
|
<b>The sovereign cloud that puts you in control</b>
|
||||||
|
<br>
|
||||||
|
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
## Recall.ai - API for meeting recordings
|
## Recall.ai - API for meeting recordings
|
||||||
@@ -42,16 +52,6 @@ an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and
|
|||||||
<b>The complete IDE crafted for professional Go developers</b>
|
<b>The complete IDE crafted for professional Go developers</b>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://github.com/beclab/Olares" target="_blank">
|
|
||||||
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
|
|
||||||
<br>
|
|
||||||
<b>The sovereign cloud that puts you in control</b>
|
|
||||||
<br>
|
|
||||||
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<!--gold sponsors end-->
|
<!--gold sponsors end-->
|
||||||
|
|
||||||
## 为什么使用 frp ?
|
## 为什么使用 frp ?
|
||||||
|
|||||||
+2
-9
@@ -1,10 +1,3 @@
|
|||||||
## Features
|
## Fixes
|
||||||
|
|
||||||
* Added a built-in `store` capability for frpc, including persisted store source (`[store] path = "..."`), Store CRUD admin APIs (`/api/store/proxies*`, `/api/store/visitors*`) with runtime reload, and Store management pages in the frpc web dashboard.
|
* Fixed a configuration-dependent authentication bypass in `type = "http"` proxies when `routeByHTTPUser` is used together with `httpUser` / `httpPassword`. This affected proxy-style requests. Proxy-style authentication failures now return `407 Proxy Authentication Required`.
|
||||||
|
|
||||||
## Improvements
|
|
||||||
|
|
||||||
* Kept proxy/visitor names as raw config names during completion; moved user-prefix handling to explicit wire-level naming logic.
|
|
||||||
* Added `noweb` build tag to allow compiling without frontend assets. `make build` now auto-detects missing `web/*/dist` directories and skips embedding, so a fresh clone can build without running `make web` first. The dashboard gracefully returns 404 when assets are not embedded.
|
|
||||||
* Improved config parsing errors: for `.toml` files, syntax errors now return immediately with parser position details (line/column when available) instead of falling through to YAML/JSON parsing, and TOML type mismatches report field-level errors without misleading line numbers.
|
|
||||||
* OIDC auth now caches the access token and refreshes it before expiry, avoiding a new token request on every heartbeat. Falls back to per-request fetch when the provider omits `expires_in`.
|
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ func (c *defaultConnectorImpl) Open() error {
|
|||||||
fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
|
fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
|
||||||
session, err := fmux.Client(conn, fmuxCfg)
|
session, err := fmux.Client(conn, fmuxCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.muxSession = session
|
c.muxSession = session
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# Release Process
|
||||||
|
|
||||||
|
## 1. Update Release Notes
|
||||||
|
|
||||||
|
Edit `Release.md` in the project root with the changes for this version:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Features
|
||||||
|
* ...
|
||||||
|
|
||||||
|
## Improvements
|
||||||
|
* ...
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
* ...
|
||||||
|
```
|
||||||
|
|
||||||
|
This file is used by GoReleaser as the GitHub Release body.
|
||||||
|
|
||||||
|
## 2. Bump Version
|
||||||
|
|
||||||
|
Update the version string in `pkg/util/version/version.go`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
var version = "0.X.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
Commit and push to `dev`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add pkg/util/version/version.go Release.md
|
||||||
|
git commit -m "bump version to vX.Y.Z"
|
||||||
|
git push origin dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Merge dev → master
|
||||||
|
|
||||||
|
Create a PR from `dev` to `master`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh pr create --base master --head dev --title "bump version"
|
||||||
|
```
|
||||||
|
|
||||||
|
Wait for CI to pass, then merge using **merge commit** (not squash).
|
||||||
|
|
||||||
|
## 4. Tag the Release
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout master
|
||||||
|
git pull origin master
|
||||||
|
git tag -a vX.Y.Z -m "bump version"
|
||||||
|
git push origin vX.Y.Z
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Trigger GoReleaser
|
||||||
|
|
||||||
|
Manually trigger the `goreleaser` workflow in GitHub Actions:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh workflow run goreleaser --ref master
|
||||||
|
```
|
||||||
|
|
||||||
|
GoReleaser will:
|
||||||
|
1. Run `package.sh` to cross-compile all platforms and create archives
|
||||||
|
2. Create a GitHub Release with all packages, using `Release.md` as release notes
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `pkg/util/version/version.go` | Version string |
|
||||||
|
| `Release.md` | Release notes (read by GoReleaser) |
|
||||||
|
| `.goreleaser.yml` | GoReleaser config |
|
||||||
|
| `package.sh` | Cross-compile and packaging script |
|
||||||
|
| `.github/workflows/goreleaser.yml` | GitHub Actions workflow (manual trigger) |
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
- Minor release: `v0.X.0`
|
||||||
|
- Patch release: `v0.X.Y` (e.g., `v0.62.1`)
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB |
@@ -1,8 +1,11 @@
|
|||||||
FROM node:22 AS web-builder
|
FROM node:22 AS web-builder
|
||||||
|
|
||||||
WORKDIR /web/frpc
|
COPY web/package.json /web/package.json
|
||||||
COPY web/frpc/ ./
|
COPY web/shared/ /web/shared/
|
||||||
|
COPY web/frpc/ /web/frpc/
|
||||||
|
WORKDIR /web
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
WORKDIR /web/frpc
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM golang:1.25 AS building
|
FROM golang:1.25 AS building
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
FROM node:22 AS web-builder
|
FROM node:22 AS web-builder
|
||||||
|
|
||||||
WORKDIR /web/frps
|
COPY web/package.json /web/package.json
|
||||||
COPY web/frps/ ./
|
COPY web/shared/ /web/shared/
|
||||||
|
COPY web/frps/ /web/frps/
|
||||||
|
WORKDIR /web
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
WORKDIR /web/frps
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM golang:1.25 AS building
|
FROM golang:1.25 AS building
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ go 1.25.0
|
|||||||
require (
|
require (
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||||
github.com/coreos/go-oidc/v3 v3.14.1
|
github.com/coreos/go-oidc/v3 v3.14.1
|
||||||
github.com/fatedier/golib v0.5.1
|
github.com/fatedier/golib v0.6.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.1
|
||||||
github.com/gorilla/websocket v1.5.0
|
github.com/gorilla/websocket v1.5.0
|
||||||
@@ -13,7 +13,7 @@ require (
|
|||||||
github.com/onsi/ginkgo/v2 v2.23.4
|
github.com/onsi/ginkgo/v2 v2.23.4
|
||||||
github.com/onsi/gomega v1.36.3
|
github.com/onsi/gomega v1.36.3
|
||||||
github.com/pelletier/go-toml/v2 v2.2.0
|
github.com/pelletier/go-toml/v2 v2.2.0
|
||||||
github.com/pion/stun/v2 v2.0.0
|
github.com/pion/stun/v3 v3.1.1
|
||||||
github.com/pires/go-proxyproto v0.7.0
|
github.com/pires/go-proxyproto v0.7.0
|
||||||
github.com/prometheus/client_golang v1.19.1
|
github.com/prometheus/client_golang v1.19.1
|
||||||
github.com/quic-go/quic-go v0.55.0
|
github.com/quic-go/quic-go v0.55.0
|
||||||
@@ -22,15 +22,15 @@ require (
|
|||||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
|
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
|
||||||
github.com/spf13/cobra v1.8.0
|
github.com/spf13/cobra v1.8.0
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/tidwall/gjson v1.17.1
|
github.com/tidwall/gjson v1.17.1
|
||||||
github.com/vishvananda/netlink v1.3.0
|
github.com/vishvananda/netlink v1.3.0
|
||||||
github.com/xtaci/kcp-go/v5 v5.6.13
|
github.com/xtaci/kcp-go/v5 v5.6.13
|
||||||
golang.org/x/crypto v0.41.0
|
golang.org/x/crypto v0.49.0
|
||||||
golang.org/x/net v0.43.0
|
golang.org/x/net v0.52.0
|
||||||
golang.org/x/oauth2 v0.28.0
|
golang.org/x/oauth2 v0.28.0
|
||||||
golang.org/x/sync v0.16.0
|
golang.org/x/sync v0.20.0
|
||||||
golang.org/x/time v0.5.0
|
golang.org/x/time v0.10.0
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||||
gopkg.in/ini.v1 v1.67.0
|
gopkg.in/ini.v1 v1.67.0
|
||||||
k8s.io/apimachinery v0.28.8
|
k8s.io/apimachinery v0.28.8
|
||||||
@@ -38,7 +38,7 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
github.com/Azure/go-ntlmssp v0.1.0 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
@@ -51,10 +51,9 @@ require (
|
|||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
||||||
github.com/klauspost/reedsolomon v1.12.0 // indirect
|
github.com/klauspost/reedsolomon v1.12.0 // indirect
|
||||||
github.com/pion/dtls/v2 v2.2.7 // indirect
|
github.com/pion/dtls/v3 v3.0.10 // indirect
|
||||||
github.com/pion/logging v0.2.2 // indirect
|
github.com/pion/logging v0.2.4 // indirect
|
||||||
github.com/pion/transport/v2 v2.2.1 // indirect
|
github.com/pion/transport/v4 v4.0.1 // indirect
|
||||||
github.com/pion/transport/v3 v3.0.1 // indirect
|
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_model v0.5.0 // indirect
|
github.com/prometheus/client_model v0.5.0 // indirect
|
||||||
@@ -66,11 +65,12 @@ require (
|
|||||||
github.com/tidwall/pretty v1.2.0 // indirect
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||||
github.com/vishvananda/netns v0.0.4 // indirect
|
github.com/vishvananda/netns v0.0.4 // indirect
|
||||||
|
github.com/wlynxg/anet v0.0.5 // indirect
|
||||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||||
golang.org/x/mod v0.27.0 // indirect
|
golang.org/x/mod v0.33.0 // indirect
|
||||||
golang.org/x/sys v0.35.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
golang.org/x/text v0.28.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
golang.org/x/tools v0.36.0 // indirect
|
golang.org/x/tools v0.42.0 // indirect
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
google.golang.org/protobuf v1.36.5 // indirect
|
google.golang.org/protobuf v1.36.5 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||||
@@ -20,8 +20,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
|||||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
github.com/fatedier/golib v0.5.1 h1:hcKAnaw5mdI/1KWRGejxR+i1Hn/NvbY5UsMKDr7o13M=
|
github.com/fatedier/golib v0.6.0 h1:/mgBZZbkbMhIEZoXf7nV8knpUDzas/b+2ruYKxx1lww=
|
||||||
github.com/fatedier/golib v0.5.1/go.mod h1:W6kIYkIFxHsTzbgqg5piCxIiDo4LzwgTY6R5W8l9NFQ=
|
github.com/fatedier/golib v0.6.0/go.mod h1:ArUGvPg2cOw/py2RAuBt46nNZH2VQ5Z70p109MAZpJw=
|
||||||
github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6 h1:u92UUy6FURPmNsMBUuongRWC0rBqN6gd01Dzu+D21NE=
|
github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6 h1:u92UUy6FURPmNsMBUuongRWC0rBqN6gd01Dzu+D21NE=
|
||||||
github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6/go.mod h1:c5/tk6G0dSpXGzJN7Wk1OEie8grdSJAmeawId9Zvd34=
|
github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6/go.mod h1:c5/tk6G0dSpXGzJN7Wk1OEie8grdSJAmeawId9Zvd34=
|
||||||
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
||||||
@@ -78,16 +78,14 @@ github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
|
|||||||
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo=
|
github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
|
github.com/pion/dtls/v3 v3.0.10 h1:k9ekkq1kaZoxnNEbyLKI8DI37j/Nbk1HWmMuywpQJgg=
|
||||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
github.com/pion/dtls/v3 v3.0.10/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8=
|
||||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||||
github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0=
|
github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw=
|
||||||
github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ=
|
github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM=
|
||||||
github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c=
|
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
||||||
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
|
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
||||||
github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
|
|
||||||
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
|
|
||||||
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
|
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
|
||||||
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
|
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
@@ -128,11 +126,10 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
|
|||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/templexxx/cpu v0.1.1 h1:isxHaxBXpYFWnk2DReuKkigaZyrjs2+9ypIdGP4h+HI=
|
github.com/templexxx/cpu v0.1.1 h1:isxHaxBXpYFWnk2DReuKkigaZyrjs2+9ypIdGP4h+HI=
|
||||||
github.com/templexxx/cpu v0.1.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk=
|
github.com/templexxx/cpu v0.1.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk=
|
||||||
github.com/templexxx/xorsimd v0.4.3 h1:9AQTFHd7Bhk3dIT7Al2XeBX5DWOvsUPZCuhyAtNbHjU=
|
github.com/templexxx/xorsimd v0.4.3 h1:9AQTFHd7Bhk3dIT7Al2XeBX5DWOvsUPZCuhyAtNbHjU=
|
||||||
@@ -149,11 +146,12 @@ github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQ
|
|||||||
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
|
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
|
||||||
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||||
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||||
|
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||||
|
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||||
github.com/xtaci/kcp-go/v5 v5.6.13 h1:FEjtz9+D4p8t2x4WjciGt/jsIuhlWjjgPCCWjrVR4Hk=
|
github.com/xtaci/kcp-go/v5 v5.6.13 h1:FEjtz9+D4p8t2x4WjciGt/jsIuhlWjjgPCCWjrVR4Hk=
|
||||||
github.com/xtaci/kcp-go/v5 v5.6.13/go.mod h1:75S1AKYYzNUSXIv30h+jPKJYZUwqpfvLshu63nCNSOM=
|
github.com/xtaci/kcp-go/v5 v5.6.13/go.mod h1:75S1AKYYzNUSXIv30h+jPKJYZUwqpfvLshu63nCNSOM=
|
||||||
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E=
|
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E=
|
||||||
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37/go.mod h1:HpMP7DB2CyokmAh4lp0EQnnWhmycP/TvwBGzvuie+H0=
|
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37/go.mod h1:HpMP7DB2CyokmAh4lp0EQnnWhmycP/TvwBGzvuie+H0=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
|
||||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||||
@@ -161,89 +159,54 @@ go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
|||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
|
||||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
|
||||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
|
||||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
|
||||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
|
||||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
|
||||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
|
||||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
|
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
|
||||||
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
|
||||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
|
||||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
|
||||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
|
||||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
|
||||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
|
||||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
|
||||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
|
||||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
|
||||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
|
||||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
|
||||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pion/stun/v2"
|
"github.com/pion/stun/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var responseTimeout = 3 * time.Second
|
var responseTimeout = 3 * time.Second
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/fatedier/golib/crypto"
|
"github.com/fatedier/golib/crypto"
|
||||||
"github.com/pion/stun/v2"
|
"github.com/pion/stun/v3"
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/msg"
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
package version
|
package version
|
||||||
|
|
||||||
var version = "0.68.0"
|
var version = "0.68.1"
|
||||||
|
|
||||||
func Full() string {
|
func Full() string {
|
||||||
return version
|
return version
|
||||||
|
|||||||
+41
-22
@@ -187,16 +187,25 @@ func (rp *HTTPReverseProxy) CreateConnection(reqRouteInfo *RequestRouteInfo, byE
|
|||||||
return nil, fmt.Errorf("%v: %s %s %s", ErrNoRouteFound, host, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
|
return nil, fmt.Errorf("%v: %s %s %s", ErrNoRouteFound, host, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rp *HTTPReverseProxy) CheckAuth(domain, location, routeByHTTPUser, user, passwd string) bool {
|
func checkRouteAuthByRequest(req *http.Request, rc *RouteConfig) bool {
|
||||||
vr, ok := rp.getVhost(domain, location, routeByHTTPUser)
|
if rc == nil {
|
||||||
if ok {
|
return true
|
||||||
checkUser := vr.payload.(*RouteConfig).Username
|
}
|
||||||
checkPasswd := vr.payload.(*RouteConfig).Password
|
if rc.Username == "" && rc.Password == "" {
|
||||||
if (checkUser != "" || checkPasswd != "") && (checkUser != user || checkPasswd != passwd) {
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.URL.Host != "" {
|
||||||
|
proxyAuth := req.Header.Get("Proxy-Authorization")
|
||||||
|
if proxyAuth == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
user, passwd, ok := httppkg.ParseBasicAuth(proxyAuth)
|
||||||
|
return ok && user == rc.Username && passwd == rc.Password
|
||||||
}
|
}
|
||||||
return true
|
|
||||||
|
user, passwd, ok := req.BasicAuth()
|
||||||
|
return ok && user == rc.Username && passwd == rc.Password
|
||||||
}
|
}
|
||||||
|
|
||||||
// getVhost tries to get vhost router by route policy.
|
// getVhost tries to get vhost router by route policy.
|
||||||
@@ -266,18 +275,25 @@ func (rp *HTTPReverseProxy) connectHandler(rw http.ResponseWriter, req *http.Req
|
|||||||
go libio.Join(remote, client)
|
go libio.Join(remote, client)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rp *HTTPReverseProxy) injectRequestInfoToCtx(req *http.Request) *http.Request {
|
func getRequestRouteUser(req *http.Request) string {
|
||||||
user := ""
|
|
||||||
// If url host isn't empty, it's a proxy request. Get http user from Proxy-Authorization header.
|
|
||||||
if req.URL.Host != "" {
|
if req.URL.Host != "" {
|
||||||
proxyAuth := req.Header.Get("Proxy-Authorization")
|
proxyAuth := req.Header.Get("Proxy-Authorization")
|
||||||
if proxyAuth != "" {
|
if proxyAuth == "" {
|
||||||
user, _, _ = httppkg.ParseBasicAuth(proxyAuth)
|
// Preserve legacy proxy-mode routing when clients send only Authorization,
|
||||||
|
// so requests still hit the matched route and return 407 instead of 404.
|
||||||
|
// Auth validation intentionally does not share this fallback.
|
||||||
|
user, _, _ := req.BasicAuth()
|
||||||
|
return user
|
||||||
}
|
}
|
||||||
|
user, _, _ := httppkg.ParseBasicAuth(proxyAuth)
|
||||||
|
return user
|
||||||
}
|
}
|
||||||
if user == "" {
|
user, _, _ := req.BasicAuth()
|
||||||
user, _, _ = req.BasicAuth()
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rp *HTTPReverseProxy) injectRequestInfoToCtx(req *http.Request) *http.Request {
|
||||||
|
user := getRequestRouteUser(req)
|
||||||
|
|
||||||
reqRouteInfo := &RequestRouteInfo{
|
reqRouteInfo := &RequestRouteInfo{
|
||||||
URL: req.URL.Path,
|
URL: req.URL.Path,
|
||||||
@@ -297,16 +313,19 @@ func (rp *HTTPReverseProxy) injectRequestInfoToCtx(req *http.Request) *http.Requ
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (rp *HTTPReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
func (rp *HTTPReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
domain, _ := httppkg.CanonicalHost(req.Host)
|
newreq := rp.injectRequestInfoToCtx(req)
|
||||||
location := req.URL.Path
|
rc := newreq.Context().Value(RouteConfigKey).(*RouteConfig)
|
||||||
user, passwd, _ := req.BasicAuth()
|
if !checkRouteAuthByRequest(req, rc) {
|
||||||
if !rp.CheckAuth(domain, location, user, user, passwd) {
|
if req.URL.Host != "" {
|
||||||
rw.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
rw.Header().Set("Proxy-Authenticate", `Basic realm="Restricted"`)
|
||||||
http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
http.Error(rw, http.StatusText(http.StatusProxyAuthRequired), http.StatusProxyAuthRequired)
|
||||||
|
} else {
|
||||||
|
rw.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||||
|
http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
newreq := rp.injectRequestInfoToCtx(req)
|
|
||||||
if req.Method == http.MethodConnect {
|
if req.Method == http.MethodConnect {
|
||||||
rp.connectHandler(rw, newreq)
|
rp.connectHandler(rw, newreq)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package vhost
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCheckRouteAuthByRequest(t *testing.T) {
|
||||||
|
rc := &RouteConfig{
|
||||||
|
Username: "alice",
|
||||||
|
Password: "secret",
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("accepts nil route config", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
|
require.True(t, checkRouteAuthByRequest(req, nil))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("accepts route without credentials", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
|
require.True(t, checkRouteAuthByRequest(req, &RouteConfig{}))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("accepts authorization header", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
|
req.SetBasicAuth("alice", "secret")
|
||||||
|
require.True(t, checkRouteAuthByRequest(req, rc))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("accepts proxy authorization header", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "http://target.example.com/", nil)
|
||||||
|
req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("alice", "secret"))
|
||||||
|
require.True(t, checkRouteAuthByRequest(req, rc))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("rejects authorization fallback for proxy request", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "http://target.example.com/", nil)
|
||||||
|
req.SetBasicAuth("alice", "secret")
|
||||||
|
require.False(t, checkRouteAuthByRequest(req, rc))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("rejects wrong proxy authorization even when authorization matches", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "http://target.example.com/", nil)
|
||||||
|
req.SetBasicAuth("alice", "secret")
|
||||||
|
req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("alice", "wrong"))
|
||||||
|
require.False(t, checkRouteAuthByRequest(req, rc))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("rejects when neither header matches", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "http://target.example.com/", nil)
|
||||||
|
req.SetBasicAuth("alice", "wrong")
|
||||||
|
req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("alice", "wrong"))
|
||||||
|
require.False(t, checkRouteAuthByRequest(req, rc))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("rejects proxy authorization on direct request", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
|
req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("alice", "secret"))
|
||||||
|
require.False(t, checkRouteAuthByRequest(req, rc))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRequestRouteUser(t *testing.T) {
|
||||||
|
t.Run("proxy request uses proxy authorization username", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "http://target.example.com/", nil)
|
||||||
|
req.Host = "target.example.com"
|
||||||
|
req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("proxy-user", "proxy-pass"))
|
||||||
|
req.SetBasicAuth("direct-user", "direct-pass")
|
||||||
|
|
||||||
|
require.Equal(t, "proxy-user", getRequestRouteUser(req))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("connect request keeps proxy authorization routing", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("CONNECT", "http://target.example.com:443", nil)
|
||||||
|
req.Host = "target.example.com:443"
|
||||||
|
req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("proxy-user", "proxy-pass"))
|
||||||
|
req.SetBasicAuth("direct-user", "direct-pass")
|
||||||
|
|
||||||
|
require.Equal(t, "proxy-user", getRequestRouteUser(req))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("direct request uses authorization username", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
|
req.Host = "example.com"
|
||||||
|
req.SetBasicAuth("direct-user", "direct-pass")
|
||||||
|
|
||||||
|
require.Equal(t, "direct-user", getRequestRouteUser(req))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("proxy request does not fall back when proxy authorization is invalid", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "http://target.example.com/", nil)
|
||||||
|
req.Host = "target.example.com"
|
||||||
|
req.Header.Set("Proxy-Authorization", "Basic !!!")
|
||||||
|
req.SetBasicAuth("direct-user", "direct-pass")
|
||||||
|
|
||||||
|
require.Empty(t, getRequestRouteUser(req))
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -241,6 +241,31 @@ func (f *Framework) AllocPort() int {
|
|||||||
return port
|
return port
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Framework) AllocPortExcludingRanges(ranges ...[2]int) int {
|
||||||
|
for range 1000 {
|
||||||
|
port := f.portAllocator.Get()
|
||||||
|
ExpectTrue(port > 0, "alloc port failed")
|
||||||
|
|
||||||
|
inExcludedRange := false
|
||||||
|
for _, portRange := range ranges {
|
||||||
|
if port >= portRange[0] && port <= portRange[1] {
|
||||||
|
inExcludedRange = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if inExcludedRange {
|
||||||
|
f.portAllocator.Release(port)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
f.allocatedPorts = append(f.allocatedPorts, port)
|
||||||
|
return port
|
||||||
|
}
|
||||||
|
|
||||||
|
Failf("alloc port outside excluded ranges failed")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
func (f *Framework) ReleasePort(port int) {
|
func (f *Framework) ReleasePort(port int) {
|
||||||
f.portAllocator.Release(port)
|
f.portAllocator.Release(port)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
|
|||||||
ginkgo.It("Ports Whitelist", func() {
|
ginkgo.It("Ports Whitelist", func() {
|
||||||
serverConf := consts.LegacyDefaultServerConfig
|
serverConf := consts.LegacyDefaultServerConfig
|
||||||
clientConf := consts.LegacyDefaultClientConfig
|
clientConf := consts.LegacyDefaultClientConfig
|
||||||
|
tcpPortNotAllowed := f.AllocPortExcludingRanges([2]int{10000, 11000}, [2]int{11002, 11002}, [2]int{12000, 13000})
|
||||||
|
udpPortNotAllowed := f.AllocPortExcludingRanges([2]int{10000, 11000}, [2]int{11002, 11002}, [2]int{12000, 13000})
|
||||||
|
|
||||||
serverConf += `
|
serverConf += `
|
||||||
allow_ports = 10000-11000,11002,12000-13000
|
allow_ports = 10000-11000,11002,12000-13000
|
||||||
@@ -37,8 +39,8 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
|
|||||||
[tcp-port-not-allowed]
|
[tcp-port-not-allowed]
|
||||||
type = tcp
|
type = tcp
|
||||||
local_port = {{ .%s }}
|
local_port = {{ .%s }}
|
||||||
remote_port = 11001
|
remote_port = %d
|
||||||
`, framework.TCPEchoServerPort)
|
`, framework.TCPEchoServerPort, tcpPortNotAllowed)
|
||||||
clientConf += fmt.Sprintf(`
|
clientConf += fmt.Sprintf(`
|
||||||
[tcp-port-unavailable]
|
[tcp-port-unavailable]
|
||||||
type = tcp
|
type = tcp
|
||||||
@@ -55,8 +57,8 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
|
|||||||
[udp-port-not-allowed]
|
[udp-port-not-allowed]
|
||||||
type = udp
|
type = udp
|
||||||
local_port = {{ .%s }}
|
local_port = {{ .%s }}
|
||||||
remote_port = 11003
|
remote_port = %d
|
||||||
`, framework.UDPEchoServerPort)
|
`, framework.UDPEchoServerPort, udpPortNotAllowed)
|
||||||
|
|
||||||
f.RunProcesses(serverConf, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
@@ -65,7 +67,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
|
|||||||
framework.NewRequestExpect(f).PortName(tcpPortName).Ensure()
|
framework.NewRequestExpect(f).PortName(tcpPortName).Ensure()
|
||||||
|
|
||||||
// Not Allowed
|
// Not Allowed
|
||||||
framework.NewRequestExpect(f).Port(11001).ExpectError(true).Ensure()
|
framework.NewRequestExpect(f).Port(tcpPortNotAllowed).ExpectError(true).Ensure()
|
||||||
|
|
||||||
// Unavailable, already bind by frps
|
// Unavailable, already bind by frps
|
||||||
framework.NewRequestExpect(f).PortName(consts.PortServerName).ExpectError(true).Ensure()
|
framework.NewRequestExpect(f).PortName(consts.PortServerName).ExpectError(true).Ensure()
|
||||||
@@ -76,7 +78,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
|
|||||||
|
|
||||||
// Not Allowed
|
// Not Allowed
|
||||||
framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
|
framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
|
||||||
r.UDP().Port(11003)
|
r.UDP().Port(udpPortNotAllowed)
|
||||||
}).ExpectError(true).Ensure()
|
}).ExpectError(true).Ensure()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -144,6 +144,79 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
|
|||||||
Ensure()
|
Ensure()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ginkgo.It("HTTP proxy mode uses proxy auth consistently", func() {
|
||||||
|
vhostHTTPPort := f.AllocPort()
|
||||||
|
serverConf := getDefaultServerConf(vhostHTTPPort)
|
||||||
|
|
||||||
|
backendPort := f.AllocPort()
|
||||||
|
f.RunServer("", newHTTPServer(backendPort, "PRIVATE"))
|
||||||
|
|
||||||
|
clientConf := consts.DefaultClientConfig
|
||||||
|
clientConf += fmt.Sprintf(`
|
||||||
|
[[proxies]]
|
||||||
|
name = "protected"
|
||||||
|
type = "http"
|
||||||
|
localPort = %d
|
||||||
|
customDomains = ["normal.example.com"]
|
||||||
|
routeByHTTPUser = "alice"
|
||||||
|
httpUser = "alice"
|
||||||
|
httpPassword = "secret"
|
||||||
|
`, backendPort)
|
||||||
|
|
||||||
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
|
proxyURLWithAuth := func(username, password string) string {
|
||||||
|
if username == "" {
|
||||||
|
return fmt.Sprintf("http://127.0.0.1:%d", vhostHTTPPort)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("http://%s:%s@127.0.0.1:%d", username, password, vhostHTTPPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
framework.NewRequestExpect(f).Explain("direct no auth").Port(vhostHTTPPort).
|
||||||
|
RequestModify(func(r *request.Request) {
|
||||||
|
r.HTTP().HTTPHost("normal.example.com")
|
||||||
|
}).
|
||||||
|
Ensure(framework.ExpectResponseCode(http.StatusNotFound))
|
||||||
|
|
||||||
|
framework.NewRequestExpect(f).Explain("direct correct auth").Port(vhostHTTPPort).
|
||||||
|
RequestModify(func(r *request.Request) {
|
||||||
|
r.HTTP().HTTPHost("normal.example.com").HTTPAuth("alice", "secret")
|
||||||
|
}).
|
||||||
|
ExpectResp([]byte("PRIVATE")).
|
||||||
|
Ensure()
|
||||||
|
|
||||||
|
framework.NewRequestExpect(f).Explain("direct wrong auth").Port(vhostHTTPPort).
|
||||||
|
RequestModify(func(r *request.Request) {
|
||||||
|
r.HTTP().HTTPHost("normal.example.com").HTTPAuth("alice", "wrong")
|
||||||
|
}).
|
||||||
|
Ensure(framework.ExpectResponseCode(http.StatusUnauthorized))
|
||||||
|
|
||||||
|
framework.NewRequestExpect(f).Explain("proxy correct proxy auth").
|
||||||
|
RequestModify(func(r *request.Request) {
|
||||||
|
r.HTTP().Addr("normal.example.com").Proxy(proxyURLWithAuth("alice", "secret"))
|
||||||
|
}).
|
||||||
|
ExpectResp([]byte("PRIVATE")).
|
||||||
|
Ensure()
|
||||||
|
|
||||||
|
framework.NewRequestExpect(f).Explain("proxy wrong proxy auth").
|
||||||
|
RequestModify(func(r *request.Request) {
|
||||||
|
r.HTTP().Addr("normal.example.com").Proxy(proxyURLWithAuth("alice", "wrong"))
|
||||||
|
}).
|
||||||
|
Ensure(framework.ExpectResponseCode(http.StatusProxyAuthRequired))
|
||||||
|
|
||||||
|
framework.NewRequestExpect(f).Explain("proxy request ignores authorization header").
|
||||||
|
RequestModify(func(r *request.Request) {
|
||||||
|
r.HTTP().Addr("normal.example.com").Proxy(proxyURLWithAuth("", "")).HTTPAuth("alice", "secret")
|
||||||
|
}).
|
||||||
|
Ensure(framework.ExpectResponseCode(http.StatusProxyAuthRequired))
|
||||||
|
|
||||||
|
framework.NewRequestExpect(f).Explain("proxy wrong proxy auth with correct authorization").
|
||||||
|
RequestModify(func(r *request.Request) {
|
||||||
|
r.HTTP().Addr("normal.example.com").Proxy(proxyURLWithAuth("alice", "wrong")).HTTPAuth("alice", "secret")
|
||||||
|
}).
|
||||||
|
Ensure(framework.ExpectResponseCode(http.StatusProxyAuthRequired))
|
||||||
|
})
|
||||||
|
|
||||||
ginkgo.It("HTTP Basic Auth", func() {
|
ginkgo.It("HTTP Basic Auth", func() {
|
||||||
vhostHTTPPort := f.AllocPort()
|
vhostHTTPPort := f.AllocPort()
|
||||||
serverConf := getDefaultServerConf(vhostHTTPPort)
|
serverConf := getDefaultServerConf(vhostHTTPPort)
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
|
|||||||
ginkgo.It("Ports Whitelist", func() {
|
ginkgo.It("Ports Whitelist", func() {
|
||||||
serverConf := consts.DefaultServerConfig
|
serverConf := consts.DefaultServerConfig
|
||||||
clientConf := consts.DefaultClientConfig
|
clientConf := consts.DefaultClientConfig
|
||||||
|
tcpPortNotAllowed := f.AllocPortExcludingRanges([2]int{10000, 11000}, [2]int{11002, 11002}, [2]int{12000, 13000})
|
||||||
|
udpPortNotAllowed := f.AllocPortExcludingRanges([2]int{10000, 11000}, [2]int{11002, 11002}, [2]int{12000, 13000})
|
||||||
|
|
||||||
serverConf += `
|
serverConf += `
|
||||||
allowPorts = [
|
allowPorts = [
|
||||||
@@ -43,8 +45,8 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
|
|||||||
name = "tcp-port-not-allowed"
|
name = "tcp-port-not-allowed"
|
||||||
type = "tcp"
|
type = "tcp"
|
||||||
localPort = {{ .%s }}
|
localPort = {{ .%s }}
|
||||||
remotePort = 11001
|
remotePort = %d
|
||||||
`, framework.TCPEchoServerPort)
|
`, framework.TCPEchoServerPort, tcpPortNotAllowed)
|
||||||
clientConf += fmt.Sprintf(`
|
clientConf += fmt.Sprintf(`
|
||||||
[[proxies]]
|
[[proxies]]
|
||||||
name = "tcp-port-unavailable"
|
name = "tcp-port-unavailable"
|
||||||
@@ -64,8 +66,8 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
|
|||||||
name = "udp-port-not-allowed"
|
name = "udp-port-not-allowed"
|
||||||
type = "udp"
|
type = "udp"
|
||||||
localPort = {{ .%s }}
|
localPort = {{ .%s }}
|
||||||
remotePort = 11003
|
remotePort = %d
|
||||||
`, framework.UDPEchoServerPort)
|
`, framework.UDPEchoServerPort, udpPortNotAllowed)
|
||||||
|
|
||||||
f.RunProcesses(serverConf, []string{clientConf})
|
f.RunProcesses(serverConf, []string{clientConf})
|
||||||
|
|
||||||
@@ -74,7 +76,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
|
|||||||
framework.NewRequestExpect(f).PortName(tcpPortName).Ensure()
|
framework.NewRequestExpect(f).PortName(tcpPortName).Ensure()
|
||||||
|
|
||||||
// Not Allowed
|
// Not Allowed
|
||||||
framework.NewRequestExpect(f).Port(11001).ExpectError(true).Ensure()
|
framework.NewRequestExpect(f).Port(tcpPortNotAllowed).ExpectError(true).Ensure()
|
||||||
|
|
||||||
// Unavailable, already bind by frps
|
// Unavailable, already bind by frps
|
||||||
framework.NewRequestExpect(f).PortName(consts.PortServerName).ExpectError(true).Ensure()
|
framework.NewRequestExpect(f).PortName(consts.PortServerName).ExpectError(true).Ensure()
|
||||||
@@ -85,7 +87,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
|
|||||||
|
|
||||||
// Not Allowed
|
// Not Allowed
|
||||||
framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
|
framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
|
||||||
r.UDP().Port(11003)
|
r.UDP().Port(udpPortNotAllowed)
|
||||||
}).ExpectError(true).Ensure()
|
}).ExpectError(true).Ensure()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
.PHONY: dist install build preview lint
|
.PHONY: dist install build preview lint
|
||||||
|
|
||||||
install:
|
install:
|
||||||
@npm install
|
@cd .. && npm install
|
||||||
|
|
||||||
build: install
|
build: install
|
||||||
@npm run build
|
@npm run build
|
||||||
|
|||||||
Vendored
-6
@@ -7,11 +7,8 @@ export {}
|
|||||||
|
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
ActionButton: typeof import('./src/components/ActionButton.vue')['default']
|
|
||||||
BaseDialog: typeof import('./src/components/BaseDialog.vue')['default']
|
|
||||||
ConfigField: typeof import('./src/components/ConfigField.vue')['default']
|
ConfigField: typeof import('./src/components/ConfigField.vue')['default']
|
||||||
ConfigSection: typeof import('./src/components/ConfigSection.vue')['default']
|
ConfigSection: typeof import('./src/components/ConfigSection.vue')['default']
|
||||||
ConfirmDialog: typeof import('./src/components/ConfirmDialog.vue')['default']
|
|
||||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||||
ElForm: typeof import('element-plus/es')['ElForm']
|
ElForm: typeof import('element-plus/es')['ElForm']
|
||||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||||
@@ -21,10 +18,7 @@ declare module 'vue' {
|
|||||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||||
FilterDropdown: typeof import('./src/components/FilterDropdown.vue')['default']
|
|
||||||
KeyValueEditor: typeof import('./src/components/KeyValueEditor.vue')['default']
|
KeyValueEditor: typeof import('./src/components/KeyValueEditor.vue')['default']
|
||||||
PopoverMenu: typeof import('./src/components/PopoverMenu.vue')['default']
|
|
||||||
PopoverMenuItem: typeof import('./src/components/PopoverMenuItem.vue')['default']
|
|
||||||
ProxyAuthSection: typeof import('./src/components/proxy-form/ProxyAuthSection.vue')['default']
|
ProxyAuthSection: typeof import('./src/components/proxy-form/ProxyAuthSection.vue')['default']
|
||||||
ProxyBackendSection: typeof import('./src/components/proxy-form/ProxyBackendSection.vue')['default']
|
ProxyBackendSection: typeof import('./src/components/proxy-form/ProxyBackendSection.vue')['default']
|
||||||
ProxyBaseSection: typeof import('./src/components/proxy-form/ProxyBaseSection.vue')['default']
|
ProxyBaseSection: typeof import('./src/components/proxy-form/ProxyBaseSection.vue')['default']
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@use './mixins' as *;
|
@use '@shared/css/mixins' as *;
|
||||||
|
|
||||||
/* Shared form layout styles for proxy/visitor form sections */
|
/* Shared form layout styles for proxy/visitor form sections */
|
||||||
.field-row {
|
.field-row {
|
||||||
|
|||||||
@@ -115,8 +115,8 @@
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import KeyValueEditor from './KeyValueEditor.vue'
|
import KeyValueEditor from './KeyValueEditor.vue'
|
||||||
import StringListEditor from './StringListEditor.vue'
|
import StringListEditor from './StringListEditor.vue'
|
||||||
import PopoverMenu from './PopoverMenu.vue'
|
import PopoverMenu from '@shared/components/PopoverMenu.vue'
|
||||||
import PopoverMenuItem from './PopoverMenuItem.vue'
|
import PopoverMenuItem from '@shared/components/PopoverMenuItem.vue'
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
|||||||
@@ -53,9 +53,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { MoreFilled, Edit, Delete, Open, TurnOff } from '@element-plus/icons-vue'
|
import { MoreFilled, Edit, Delete, Open, TurnOff } from '@element-plus/icons-vue'
|
||||||
import ActionButton from './ActionButton.vue'
|
import ActionButton from '@shared/components/ActionButton.vue'
|
||||||
import PopoverMenu from './PopoverMenu.vue'
|
import PopoverMenu from '@shared/components/PopoverMenu.vue'
|
||||||
import PopoverMenuItem from './PopoverMenuItem.vue'
|
import PopoverMenuItem from '@shared/components/PopoverMenuItem.vue'
|
||||||
import type { ProxyStatus } from '../types'
|
import type { ProxyStatus } from '../types'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import ClientConfigure from '../views/ClientConfigure.vue'
|
import ClientConfigure from '../views/ClientConfigure.vue'
|
||||||
|
import ProxyDetail from '../views/ProxyDetail.vue'
|
||||||
import ProxyEdit from '../views/ProxyEdit.vue'
|
import ProxyEdit from '../views/ProxyEdit.vue'
|
||||||
|
import ProxyList from '../views/ProxyList.vue'
|
||||||
|
import VisitorDetail from '../views/VisitorDetail.vue'
|
||||||
import VisitorEdit from '../views/VisitorEdit.vue'
|
import VisitorEdit from '../views/VisitorEdit.vue'
|
||||||
|
import VisitorList from '../views/VisitorList.vue'
|
||||||
import { useProxyStore } from '../stores/proxy'
|
import { useProxyStore } from '../stores/proxy'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
@@ -15,12 +19,12 @@ const router = createRouter({
|
|||||||
{
|
{
|
||||||
path: '/proxies',
|
path: '/proxies',
|
||||||
name: 'ProxyList',
|
name: 'ProxyList',
|
||||||
component: () => import('../views/ProxyList.vue'),
|
component: ProxyList,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/proxies/detail/:name',
|
path: '/proxies/detail/:name',
|
||||||
name: 'ProxyDetail',
|
name: 'ProxyDetail',
|
||||||
component: () => import('../views/ProxyDetail.vue'),
|
component: ProxyDetail,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/proxies/create',
|
path: '/proxies/create',
|
||||||
@@ -37,12 +41,12 @@ const router = createRouter({
|
|||||||
{
|
{
|
||||||
path: '/visitors',
|
path: '/visitors',
|
||||||
name: 'VisitorList',
|
name: 'VisitorList',
|
||||||
component: () => import('../views/VisitorList.vue'),
|
component: VisitorList,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/visitors/detail/:name',
|
path: '/visitors/detail/:name',
|
||||||
name: 'VisitorDetail',
|
name: 'VisitorDetail',
|
||||||
component: () => import('../views/VisitorDetail.vue'),
|
component: VisitorDetail,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/visitors/create',
|
path: '/visitors/create',
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ serverPort = 7000"
|
|||||||
message="This operation will update your frpc configuration and reload it. Do you want to continue?"
|
message="This operation will update your frpc configuration and reload it. Do you want to continue?"
|
||||||
confirm-text="Update"
|
confirm-text="Update"
|
||||||
:loading="uploading"
|
:loading="uploading"
|
||||||
|
:is-mobile="isMobile"
|
||||||
@confirm="doUpload"
|
@confirm="doUpload"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,9 +52,11 @@ import { ref } from 'vue'
|
|||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { Link } from '@element-plus/icons-vue'
|
import { Link } from '@element-plus/icons-vue'
|
||||||
import { useClientStore } from '../stores/client'
|
import { useClientStore } from '../stores/client'
|
||||||
import ActionButton from '../components/ActionButton.vue'
|
import ActionButton from '@shared/components/ActionButton.vue'
|
||||||
import ConfirmDialog from '../components/ConfirmDialog.vue'
|
import ConfirmDialog from '@shared/components/ConfirmDialog.vue'
|
||||||
|
import { useResponsive } from '../composables/useResponsive'
|
||||||
|
|
||||||
|
const { isMobile } = useResponsive()
|
||||||
const clientStore = useClientStore()
|
const clientStore = useClientStore()
|
||||||
const configContent = ref('')
|
const configContent = ref('')
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ import { ref, computed, onMounted } from 'vue'
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { Warning } from '@element-plus/icons-vue'
|
import { Warning } from '@element-plus/icons-vue'
|
||||||
import ActionButton from '../components/ActionButton.vue'
|
import ActionButton from '@shared/components/ActionButton.vue'
|
||||||
import ProxyFormLayout from '../components/proxy-form/ProxyFormLayout.vue'
|
import ProxyFormLayout from '../components/proxy-form/ProxyFormLayout.vue'
|
||||||
import { getProxyConfig, getStoreProxy } from '../api/frpc'
|
import { getProxyConfig, getStoreProxy } from '../api/frpc'
|
||||||
import { useProxyStore } from '../stores/proxy'
|
import { useProxyStore } from '../stores/proxy'
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
v-model="leaveDialogVisible"
|
v-model="leaveDialogVisible"
|
||||||
title="Unsaved Changes"
|
title="Unsaved Changes"
|
||||||
message="You have unsaved changes. Are you sure you want to leave?"
|
message="You have unsaved changes. Are you sure you want to leave?"
|
||||||
|
:is-mobile="isMobile"
|
||||||
@confirm="handleLeaveConfirm"
|
@confirm="handleLeaveConfirm"
|
||||||
@cancel="handleLeaveCancel"
|
@cancel="handleLeaveCancel"
|
||||||
/>
|
/>
|
||||||
@@ -50,10 +51,12 @@ import {
|
|||||||
} from '../types'
|
} from '../types'
|
||||||
import { getStoreProxy } from '../api/frpc'
|
import { getStoreProxy } from '../api/frpc'
|
||||||
import { useProxyStore } from '../stores/proxy'
|
import { useProxyStore } from '../stores/proxy'
|
||||||
import ActionButton from '../components/ActionButton.vue'
|
import ActionButton from '@shared/components/ActionButton.vue'
|
||||||
import ConfirmDialog from '../components/ConfirmDialog.vue'
|
import ConfirmDialog from '@shared/components/ConfirmDialog.vue'
|
||||||
import ProxyFormLayout from '../components/proxy-form/ProxyFormLayout.vue'
|
import ProxyFormLayout from '../components/proxy-form/ProxyFormLayout.vue'
|
||||||
|
import { useResponsive } from '../composables/useResponsive'
|
||||||
|
|
||||||
|
const { isMobile } = useResponsive()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const proxyStore = useProxyStore()
|
const proxyStore = useProxyStore()
|
||||||
|
|||||||
@@ -30,8 +30,8 @@
|
|||||||
<el-input v-model="searchText" placeholder="Search..." clearable class="search-input">
|
<el-input v-model="searchText" placeholder="Search..." clearable class="search-input">
|
||||||
<template #prefix><el-icon><Search /></el-icon></template>
|
<template #prefix><el-icon><Search /></el-icon></template>
|
||||||
</el-input>
|
</el-input>
|
||||||
<FilterDropdown v-model="sourceFilter" label="Source" :options="sourceOptions" :min-width="140" />
|
<FilterDropdown v-model="sourceFilter" label="Source" :options="sourceOptions" :min-width="140" :is-mobile="isMobile" />
|
||||||
<FilterDropdown v-model="typeFilter" label="Type" :options="typeOptions" :min-width="140" />
|
<FilterDropdown v-model="typeFilter" label="Type" :options="typeOptions" :min-width="140" :is-mobile="isMobile" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
<el-input v-model="storeSearch" placeholder="Search..." clearable class="search-input">
|
<el-input v-model="storeSearch" placeholder="Search..." clearable class="search-input">
|
||||||
<template #prefix><el-icon><Search /></el-icon></template>
|
<template #prefix><el-icon><Search /></el-icon></template>
|
||||||
</el-input>
|
</el-input>
|
||||||
<FilterDropdown v-model="storeTypeFilter" label="Type" :options="storeTypeOptions" :min-width="140" />
|
<FilterDropdown v-model="storeTypeFilter" label="Type" :options="storeTypeOptions" :min-width="140" :is-mobile="isMobile" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,6 +100,7 @@ path = "./frpc_store.json"</pre>
|
|||||||
confirm-text="Delete"
|
confirm-text="Delete"
|
||||||
danger
|
danger
|
||||||
:loading="deleteDialog.loading"
|
:loading="deleteDialog.loading"
|
||||||
|
:is-mobile="isMobile"
|
||||||
@confirm="doDelete"
|
@confirm="doDelete"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -110,11 +111,11 @@ import { ref, computed, reactive, onMounted } from 'vue'
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||||
import ActionButton from '../components/ActionButton.vue'
|
import ActionButton from '@shared/components/ActionButton.vue'
|
||||||
import StatusPills from '../components/StatusPills.vue'
|
import StatusPills from '../components/StatusPills.vue'
|
||||||
import FilterDropdown from '../components/FilterDropdown.vue'
|
import FilterDropdown from '@shared/components/FilterDropdown.vue'
|
||||||
import ProxyCard from '../components/ProxyCard.vue'
|
import ProxyCard from '../components/ProxyCard.vue'
|
||||||
import ConfirmDialog from '../components/ConfirmDialog.vue'
|
import ConfirmDialog from '@shared/components/ConfirmDialog.vue'
|
||||||
import { useProxyStore } from '../stores/proxy'
|
import { useProxyStore } from '../stores/proxy'
|
||||||
import { useResponsive } from '../composables/useResponsive'
|
import { useResponsive } from '../composables/useResponsive'
|
||||||
import type { ProxyStatus } from '../types'
|
import type { ProxyStatus } from '../types'
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import ActionButton from '../components/ActionButton.vue'
|
import ActionButton from '@shared/components/ActionButton.vue'
|
||||||
import VisitorFormLayout from '../components/visitor-form/VisitorFormLayout.vue'
|
import VisitorFormLayout from '../components/visitor-form/VisitorFormLayout.vue'
|
||||||
import { getVisitorConfig, getStoreVisitor } from '../api/frpc'
|
import { getVisitorConfig, getStoreVisitor } from '../api/frpc'
|
||||||
import type { VisitorDefinition, VisitorFormData } from '../types'
|
import type { VisitorDefinition, VisitorFormData } from '../types'
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
v-model="leaveDialogVisible"
|
v-model="leaveDialogVisible"
|
||||||
title="Unsaved Changes"
|
title="Unsaved Changes"
|
||||||
message="You have unsaved changes. Are you sure you want to leave?"
|
message="You have unsaved changes. Are you sure you want to leave?"
|
||||||
|
:is-mobile="isMobile"
|
||||||
@confirm="handleLeaveConfirm"
|
@confirm="handleLeaveConfirm"
|
||||||
@cancel="handleLeaveCancel"
|
@cancel="handleLeaveCancel"
|
||||||
/>
|
/>
|
||||||
@@ -40,9 +41,10 @@
|
|||||||
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
||||||
import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router'
|
import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import ActionButton from '../components/ActionButton.vue'
|
import ActionButton from '@shared/components/ActionButton.vue'
|
||||||
import ConfirmDialog from '../components/ConfirmDialog.vue'
|
import ConfirmDialog from '@shared/components/ConfirmDialog.vue'
|
||||||
import VisitorFormLayout from '../components/visitor-form/VisitorFormLayout.vue'
|
import VisitorFormLayout from '../components/visitor-form/VisitorFormLayout.vue'
|
||||||
|
import { useResponsive } from '../composables/useResponsive'
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
import {
|
import {
|
||||||
type VisitorFormData,
|
type VisitorFormData,
|
||||||
@@ -53,6 +55,7 @@ import {
|
|||||||
import { getStoreVisitor } from '../api/frpc'
|
import { getStoreVisitor } from '../api/frpc'
|
||||||
import { useVisitorStore } from '../stores/visitor'
|
import { useVisitorStore } from '../stores/visitor'
|
||||||
|
|
||||||
|
const { isMobile } = useResponsive()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const visitorStore = useVisitorStore()
|
const visitorStore = useVisitorStore()
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ path = "./frpc_store.json"</pre>
|
|||||||
<el-input v-model="searchText" placeholder="Search..." clearable class="search-input">
|
<el-input v-model="searchText" placeholder="Search..." clearable class="search-input">
|
||||||
<template #prefix><el-icon><Search /></el-icon></template>
|
<template #prefix><el-icon><Search /></el-icon></template>
|
||||||
</el-input>
|
</el-input>
|
||||||
<FilterDropdown v-model="typeFilter" label="Type" :options="typeOptions" :min-width="140" />
|
<FilterDropdown v-model="typeFilter" label="Type" :options="typeOptions" :min-width="140" :is-mobile="isMobile" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="filteredVisitors.length > 0" class="visitor-list">
|
<div v-if="filteredVisitors.length > 0" class="visitor-list">
|
||||||
@@ -74,7 +74,7 @@ path = "./frpc_store.json"</pre>
|
|||||||
|
|
||||||
<ConfirmDialog v-model="deleteDialog.visible" title="Delete Visitor"
|
<ConfirmDialog v-model="deleteDialog.visible" title="Delete Visitor"
|
||||||
:message="deleteDialog.message" confirm-text="Delete" danger
|
:message="deleteDialog.message" confirm-text="Delete" danger
|
||||||
:loading="deleteDialog.loading" @confirm="doDelete" />
|
:loading="deleteDialog.loading" :is-mobile="isMobile" @confirm="doDelete" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -83,14 +83,16 @@ import { ref, computed, reactive, onMounted } from 'vue'
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { Search, Refresh, MoreFilled, Edit, Delete } from '@element-plus/icons-vue'
|
import { Search, Refresh, MoreFilled, Edit, Delete } from '@element-plus/icons-vue'
|
||||||
import ActionButton from '../components/ActionButton.vue'
|
import ActionButton from '@shared/components/ActionButton.vue'
|
||||||
import FilterDropdown from '../components/FilterDropdown.vue'
|
import FilterDropdown from '@shared/components/FilterDropdown.vue'
|
||||||
import PopoverMenu from '../components/PopoverMenu.vue'
|
import PopoverMenu from '@shared/components/PopoverMenu.vue'
|
||||||
import PopoverMenuItem from '../components/PopoverMenuItem.vue'
|
import PopoverMenuItem from '@shared/components/PopoverMenuItem.vue'
|
||||||
import ConfirmDialog from '../components/ConfirmDialog.vue'
|
import ConfirmDialog from '@shared/components/ConfirmDialog.vue'
|
||||||
import { useVisitorStore } from '../stores/visitor'
|
import { useVisitorStore } from '../stores/visitor'
|
||||||
|
import { useResponsive } from '../composables/useResponsive'
|
||||||
import type { VisitorDefinition } from '../types'
|
import type { VisitorDefinition } from '../types'
|
||||||
|
|
||||||
|
const { isMobile } = useResponsive()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const visitorStore = useVisitorStore()
|
const visitorStore = useVisitorStore()
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,12 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noFallthroughCasesInSwitch": true
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"],
|
||||||
|
"@shared/*": ["../shared/*"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "../shared/**/*.ts", "../shared/**/*.vue"],
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,13 +25,19 @@ export default defineConfig({
|
|||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
'@shared': fileURLToPath(new URL('../shared', import.meta.url)),
|
||||||
},
|
},
|
||||||
|
dedupe: ['vue', 'element-plus', '@element-plus/icons-vue'],
|
||||||
|
modules: [
|
||||||
|
fileURLToPath(new URL('../node_modules', import.meta.url)),
|
||||||
|
'node_modules',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
css: {
|
css: {
|
||||||
preprocessorOptions: {
|
preprocessorOptions: {
|
||||||
scss: {
|
scss: {
|
||||||
api: 'modern',
|
api: 'modern',
|
||||||
additionalData: `@use "@/assets/css/_index.scss" as *;`,
|
additionalData: `@use "@shared/css/_index.scss" as *;`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
.PHONY: dist install build preview lint
|
.PHONY: dist install build preview lint
|
||||||
|
|
||||||
install:
|
install:
|
||||||
@npm install
|
@cd .. && npm install
|
||||||
|
|
||||||
build: install
|
build: install
|
||||||
@npm run build
|
@npm run build
|
||||||
|
|||||||
Vendored
+2
-3
@@ -11,13 +11,12 @@ declare module 'vue' {
|
|||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
ElCard: typeof import('element-plus/es')['ElCard']
|
ElCard: typeof import('element-plus/es')['ElCard']
|
||||||
ElCol: typeof import('element-plus/es')['ElCol']
|
ElCol: typeof import('element-plus/es')['ElCol']
|
||||||
|
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||||
ElEmpty: typeof import('element-plus/es')['ElEmpty']
|
ElEmpty: typeof import('element-plus/es')['ElEmpty']
|
||||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||||
ElInput: typeof import('element-plus/es')['ElInput']
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
ElOption: typeof import('element-plus/es')['ElOption']
|
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||||
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
|
|
||||||
ElRow: typeof import('element-plus/es')['ElRow']
|
ElRow: typeof import('element-plus/es')['ElRow']
|
||||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
|
||||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||||
ElTag: typeof import('element-plus/es')['ElTag']
|
ElTag: typeof import('element-plus/es')['ElTag']
|
||||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||||
|
|||||||
Generated
-7646
File diff suppressed because it is too large
Load Diff
+367
-120
@@ -2,135 +2,195 @@
|
|||||||
<div id="app">
|
<div id="app">
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
<div class="header-top">
|
<div class="brand-section">
|
||||||
<div class="brand-section">
|
<button
|
||||||
<div class="logo-wrapper">
|
v-if="isMobile"
|
||||||
<LogoIcon class="logo-icon" />
|
class="hamburger-btn"
|
||||||
</div>
|
@click="toggleSidebar"
|
||||||
<span class="divider">/</span>
|
aria-label="Toggle menu"
|
||||||
<span class="brand-name">frp</span>
|
>
|
||||||
<span class="badge server-badge">Server</span>
|
<span class="hamburger-icon">☰</span>
|
||||||
<span class="badge" v-if="currentRouteName">{{
|
</button>
|
||||||
currentRouteName
|
<div class="logo-wrapper">
|
||||||
}}</span>
|
<LogoIcon class="logo-icon" />
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="header-controls">
|
|
||||||
<a
|
|
||||||
class="github-link"
|
|
||||||
href="https://github.com/fatedier/frp"
|
|
||||||
target="_blank"
|
|
||||||
aria-label="GitHub"
|
|
||||||
>
|
|
||||||
<GitHubIcon class="github-icon" />
|
|
||||||
</a>
|
|
||||||
<el-switch
|
|
||||||
v-model="isDark"
|
|
||||||
inline-prompt
|
|
||||||
:active-icon="Moon"
|
|
||||||
:inactive-icon="Sunny"
|
|
||||||
class="theme-switch"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<span class="divider">/</span>
|
||||||
|
<span class="brand-name">frp</span>
|
||||||
|
<span class="badge server-badge">Server</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="nav-bar">
|
<div class="header-controls">
|
||||||
<router-link to="/" class="nav-link" active-class="active"
|
<a
|
||||||
>Overview</router-link
|
class="github-link"
|
||||||
|
href="https://github.com/fatedier/frp"
|
||||||
|
target="_blank"
|
||||||
|
aria-label="GitHub"
|
||||||
>
|
>
|
||||||
<router-link to="/clients" class="nav-link" active-class="active"
|
<GitHubIcon class="github-icon" />
|
||||||
>Clients</router-link
|
</a>
|
||||||
>
|
<el-switch
|
||||||
<router-link
|
v-model="isDark"
|
||||||
to="/proxies"
|
inline-prompt
|
||||||
class="nav-link"
|
:active-icon="Moon"
|
||||||
:class="{ active: route.path.startsWith('/proxies') }"
|
:inactive-icon="Sunny"
|
||||||
>Proxies</router-link
|
class="theme-switch"
|
||||||
>
|
/>
|
||||||
</nav>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main id="content">
|
<div class="layout">
|
||||||
<router-view></router-view>
|
<!-- Mobile overlay -->
|
||||||
</main>
|
<div
|
||||||
|
v-if="isMobile && sidebarOpen"
|
||||||
|
class="sidebar-overlay"
|
||||||
|
@click="closeSidebar"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<aside
|
||||||
|
class="sidebar"
|
||||||
|
:class="{ 'mobile-open': isMobile && sidebarOpen }"
|
||||||
|
>
|
||||||
|
<nav class="sidebar-nav">
|
||||||
|
<router-link
|
||||||
|
to="/"
|
||||||
|
class="sidebar-link"
|
||||||
|
:class="{ active: route.path === '/' }"
|
||||||
|
@click="closeSidebar"
|
||||||
|
>
|
||||||
|
Overview
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
to="/clients"
|
||||||
|
class="sidebar-link"
|
||||||
|
:class="{ active: route.path.startsWith('/clients') }"
|
||||||
|
@click="closeSidebar"
|
||||||
|
>
|
||||||
|
Clients
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
to="/proxies"
|
||||||
|
class="sidebar-link"
|
||||||
|
:class="{
|
||||||
|
active:
|
||||||
|
route.path.startsWith('/proxies') ||
|
||||||
|
route.path.startsWith('/proxy'),
|
||||||
|
}"
|
||||||
|
@click="closeSidebar"
|
||||||
|
>
|
||||||
|
Proxies
|
||||||
|
</router-link>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main id="content">
|
||||||
|
<router-view></router-view>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { useDark } from '@vueuse/core'
|
import { useDark } from '@vueuse/core'
|
||||||
import { Moon, Sunny } from '@element-plus/icons-vue'
|
import { Moon, Sunny } from '@element-plus/icons-vue'
|
||||||
import GitHubIcon from './assets/icons/github.svg?component'
|
import GitHubIcon from './assets/icons/github.svg?component'
|
||||||
import LogoIcon from './assets/icons/logo.svg?component'
|
import LogoIcon from './assets/icons/logo.svg?component'
|
||||||
|
import { useResponsive } from './composables/useResponsive'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const isDark = useDark()
|
const isDark = useDark()
|
||||||
|
const { isMobile } = useResponsive()
|
||||||
|
|
||||||
const currentRouteName = computed(() => {
|
const sidebarOpen = ref(false)
|
||||||
if (route.path === '/') return 'Overview'
|
|
||||||
if (route.path.startsWith('/clients')) return 'Clients'
|
const toggleSidebar = () => {
|
||||||
if (route.path.startsWith('/proxies')) return 'Proxies'
|
sidebarOpen.value = !sidebarOpen.value
|
||||||
return ''
|
}
|
||||||
})
|
|
||||||
|
const closeSidebar = () => {
|
||||||
|
sidebarOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-close sidebar on route change
|
||||||
|
watch(
|
||||||
|
() => route.path,
|
||||||
|
() => {
|
||||||
|
if (isMobile.value) {
|
||||||
|
closeSidebar()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--header-height: 112px;
|
--header-height: 50px;
|
||||||
--header-bg: rgba(255, 255, 255, 0.8);
|
--sidebar-width: 200px;
|
||||||
--header-border: #eaeaea;
|
--header-bg: #ffffff;
|
||||||
--text-primary: #000;
|
--header-border: #e4e7ed;
|
||||||
--text-secondary: #666;
|
--sidebar-bg: #ffffff;
|
||||||
--hover-bg: #f5f5f5;
|
--text-primary: #303133;
|
||||||
--active-link: #000;
|
--text-secondary: #606266;
|
||||||
|
--text-muted: #909399;
|
||||||
|
--hover-bg: #efefef;
|
||||||
|
--content-bg: #f9f9f9;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.dark {
|
html.dark {
|
||||||
--header-bg: rgba(0, 0, 0, 0.8);
|
--header-bg: #1e1e2e;
|
||||||
--header-border: #333;
|
--header-border: #3a3d5c;
|
||||||
--text-primary: #fff;
|
--sidebar-bg: #1e1e2e;
|
||||||
--text-secondary: #888;
|
--text-primary: #e5e7eb;
|
||||||
--hover-bg: #1a1a1a;
|
--text-secondary: #b0b0b0;
|
||||||
--active-link: #fff;
|
--text-muted: #888888;
|
||||||
|
--hover-bg: #2a2a3e;
|
||||||
|
--content-bg: #181825;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family:
|
font-family:
|
||||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue',
|
ui-sans-serif, -apple-system, system-ui, Segoe UI, Helvetica, Arial,
|
||||||
Arial, sans-serif;
|
sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
:after,
|
||||||
|
:before {
|
||||||
|
box-sizing: border-box;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
min-height: 100vh;
|
height: 100vh;
|
||||||
|
height: 100dvh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: var(--el-bg-color-page);
|
background-color: var(--content-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
.header {
|
.header {
|
||||||
position: sticky;
|
flex-shrink: 0;
|
||||||
top: 0;
|
|
||||||
z-index: 100;
|
|
||||||
background: var(--header-bg);
|
background: var(--header-bg);
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
-webkit-backdrop-filter: blur(12px);
|
|
||||||
border-bottom: 1px solid var(--header-border);
|
border-bottom: 1px solid var(--header-border);
|
||||||
|
height: var(--header-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-content {
|
.header-content {
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-top {
|
|
||||||
height: 64px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-section {
|
.brand-section {
|
||||||
@@ -145,13 +205,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.logo-icon {
|
.logo-icon {
|
||||||
width: 32px;
|
width: 28px;
|
||||||
height: 32px;
|
height: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
color: var(--header-border);
|
color: var(--header-border);
|
||||||
font-size: 24px;
|
font-size: 22px;
|
||||||
font-weight: 200;
|
font-weight: 200;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,12 +223,12 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.badge {
|
.badge {
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
color: var(--text-secondary);
|
font-weight: 500;
|
||||||
|
color: var(--text-muted);
|
||||||
background: var(--hover-bg);
|
background: var(--hover-bg);
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border-radius: 99px;
|
border-radius: 4px;
|
||||||
border: 1px solid var(--header-border);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge.server-badge {
|
.badge.server-badge {
|
||||||
@@ -189,17 +249,20 @@ html.dark .badge.server-badge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.github-link {
|
.github-link {
|
||||||
width: 26px;
|
|
||||||
height: 26px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: 50%;
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-link:hover {
|
||||||
|
background: var(--hover-bg);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
transition: background 0.2s;
|
|
||||||
background: transparent;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.github-icon {
|
.github-icon {
|
||||||
@@ -207,11 +270,6 @@ html.dark .badge.server-badge {
|
|||||||
height: 18px;
|
height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.github-link:hover {
|
|
||||||
background: var(--hover-bg);
|
|
||||||
border-color: var(--header-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-switch {
|
.theme-switch {
|
||||||
--el-switch-on-color: #2c2c3a;
|
--el-switch-on-color: #2c2c3a;
|
||||||
--el-switch-off-color: #f2f2f2;
|
--el-switch-off-color: #f2f2f2;
|
||||||
@@ -226,46 +284,235 @@ html.dark .theme-switch {
|
|||||||
color: #909399 !important;
|
color: #909399 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-bar {
|
/* Layout */
|
||||||
height: 48px;
|
.layout {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
.sidebar {
|
||||||
|
width: var(--sidebar-width);
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: var(--sidebar-bg);
|
||||||
|
border-right: 1px solid var(--header-border);
|
||||||
|
padding: 16px 12px;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-link {
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-link:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: var(--hover-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-link.active {
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: var(--hover-bg);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hamburger button (mobile only) */
|
||||||
|
.hamburger-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 24px;
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
transition: background 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link {
|
.hamburger-btn:hover {
|
||||||
text-decoration: none;
|
background: var(--hover-bg);
|
||||||
font-size: 14px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
padding: 8px 0;
|
|
||||||
border-bottom: 2px solid transparent;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link:hover {
|
.hamburger-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link.active {
|
/* Mobile overlay */
|
||||||
color: var(--active-link);
|
.sidebar-overlay {
|
||||||
border-bottom-color: var(--active-link);
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 99;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Content */
|
||||||
#content {
|
#content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%;
|
min-width: 0;
|
||||||
|
overflow-y: auto;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
#content > * {
|
||||||
|
max-width: 1024px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Common page styles */
|
||||||
|
.page-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-primary, var(--text-primary));
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-text-muted, var(--text-muted));
|
||||||
|
margin: 8px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Element Plus global overrides */
|
||||||
|
.el-button {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tag {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-switch {
|
||||||
|
--el-switch-on-color: #606266;
|
||||||
|
--el-switch-off-color: #dcdfe6;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .el-switch {
|
||||||
|
--el-switch-on-color: #b0b0b0;
|
||||||
|
--el-switch-off-color: #3a3d5c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form-item {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-loading-mask {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Select overrides */
|
||||||
|
.el-select__wrapper {
|
||||||
|
border-radius: 8px !important;
|
||||||
|
box-shadow: 0 0 0 1px var(--color-border-light, #e4e7ed) inset !important;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-select__wrapper:hover {
|
||||||
|
box-shadow: 0 0 0 1px var(--color-border, #dcdfe6) inset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-select__wrapper.is-focused {
|
||||||
|
box-shadow: 0 0 0 1px var(--color-border, #dcdfe6) inset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-select-dropdown {
|
||||||
|
border-radius: 12px !important;
|
||||||
|
border: 1px solid var(--color-border-light, #e4e7ed) !important;
|
||||||
|
box-shadow:
|
||||||
|
0 10px 25px -5px rgba(0, 0, 0, 0.1),
|
||||||
|
0 8px 10px -6px rgba(0, 0, 0, 0.1) !important;
|
||||||
|
padding: 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-select-dropdown__item {
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 2px 0;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-select-dropdown__item.is-selected {
|
||||||
|
color: var(--color-text-primary, var(--text-primary));
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input overrides */
|
||||||
|
.el-input__wrapper {
|
||||||
|
border-radius: 8px !important;
|
||||||
|
box-shadow: 0 0 0 1px var(--color-border-light, #e4e7ed) inset !important;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input__wrapper:hover {
|
||||||
|
box-shadow: 0 0 0 1px var(--color-border, #dcdfe6) inset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input__wrapper.is-focus {
|
||||||
|
box-shadow: 0 0 0 1px var(--color-border, #dcdfe6) inset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card overrides */
|
||||||
|
.el-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--color-border-light, #e4e7ed);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #d1d1d1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile */
|
||||||
|
@media (max-width: 767px) {
|
||||||
.header-content {
|
.header-content {
|
||||||
padding: 0 20px;
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
top: var(--header-height);
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background: var(--sidebar-bg);
|
||||||
|
transform: translateX(-100%);
|
||||||
|
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
border-right: 1px solid var(--header-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.mobile-open {
|
||||||
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#content {
|
#content {
|
||||||
|
width: 100%;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
.el-form-item span {
|
|
||||||
margin-left: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.proxy-table-expand {
|
|
||||||
font-size: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.proxy-table-expand .el-form-item__label{
|
|
||||||
width: 90px;
|
|
||||||
color: #99a9bf;
|
|
||||||
}
|
|
||||||
|
|
||||||
.proxy-table-expand .el-form-item {
|
|
||||||
margin-right: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-table .el-table__expanded-cell {
|
|
||||||
padding: 20px 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modern styles */
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Smooth transitions */
|
|
||||||
.el-button,
|
|
||||||
.el-card,
|
|
||||||
.el-input,
|
|
||||||
.el-select,
|
|
||||||
.el-tag {
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Card hover effects */
|
|
||||||
.el-card:hover {
|
|
||||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Better scrollbar */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: #f1f1f1;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: #c1c1c1;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: #a8a8a8;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Page headers */
|
|
||||||
.el-page-header {
|
|
||||||
padding: 16px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-page-header__title {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Better form layouts */
|
|
||||||
.el-form-item {
|
|
||||||
margin-bottom: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive adjustments */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.el-row {
|
|
||||||
margin-left: 0 !important;
|
|
||||||
margin-right: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-col {
|
|
||||||
padding-left: 10px !important;
|
|
||||||
padding-right: 10px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +1,153 @@
|
|||||||
|
/* Dark mode styles */
|
||||||
html.dark {
|
html.dark {
|
||||||
--el-bg-color: #1e1e2e;
|
--el-bg-color: #1e1e2e;
|
||||||
|
--el-bg-color-page: #181825;
|
||||||
|
--el-bg-color-overlay: #27293d;
|
||||||
--el-fill-color-blank: #1e1e2e;
|
--el-fill-color-blank: #1e1e2e;
|
||||||
|
--el-border-color: #3a3d5c;
|
||||||
|
--el-border-color-light: #313348;
|
||||||
|
--el-border-color-lighter: #2a2a3e;
|
||||||
|
--el-text-color-primary: #e5e7eb;
|
||||||
|
--el-text-color-secondary: #888888;
|
||||||
|
--el-text-color-placeholder: #afafaf;
|
||||||
background-color: #1e1e2e;
|
background-color: #1e1e2e;
|
||||||
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.dark body {
|
/* Scrollbar */
|
||||||
background-color: #1e1e2e;
|
html.dark ::-webkit-scrollbar {
|
||||||
color: #e5e7eb;
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark mode scrollbar */
|
|
||||||
html.dark ::-webkit-scrollbar-track {
|
html.dark ::-webkit-scrollbar-track {
|
||||||
background: #27293d;
|
background: #27293d;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.dark ::-webkit-scrollbar-thumb {
|
html.dark ::-webkit-scrollbar-thumb {
|
||||||
background: #3a3d5c;
|
background: #3a3d5c;
|
||||||
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.dark ::-webkit-scrollbar-thumb:hover {
|
html.dark ::-webkit-scrollbar-thumb:hover {
|
||||||
background: #4a4d6c;
|
background: #4a4d6c;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark mode cards */
|
/* Form */
|
||||||
html.dark .el-card {
|
html.dark .el-form-item__label {
|
||||||
background-color: #27293d;
|
color: #e5e7eb;
|
||||||
border-color: #3a3d5c;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark mode inputs */
|
/* Input */
|
||||||
html.dark .el-input__wrapper {
|
html.dark .el-input__wrapper {
|
||||||
background-color: #27293d;
|
background: var(--color-bg-input);
|
||||||
border-color: #3a3d5c;
|
box-shadow: 0 0 0 1px #3a3d5c inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .el-input__wrapper:hover {
|
||||||
|
box-shadow: 0 0 0 1px #4a4d6c inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .el-input__wrapper.is-focus {
|
||||||
|
box-shadow: 0 0 0 1px var(--el-color-primary) inset;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.dark .el-input__inner {
|
html.dark .el-input__inner {
|
||||||
color: #e5e7eb;
|
color: #e5e7eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark mode table */
|
html.dark .el-input__inner::placeholder {
|
||||||
|
color: #afafaf;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .el-textarea__inner {
|
||||||
|
background: var(--color-bg-input);
|
||||||
|
box-shadow: 0 0 0 1px #3a3d5c inset;
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .el-textarea__inner:hover {
|
||||||
|
box-shadow: 0 0 0 1px #4a4d6c inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .el-textarea__inner:focus {
|
||||||
|
box-shadow: 0 0 0 1px var(--el-color-primary) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Select */
|
||||||
|
html.dark .el-select__wrapper {
|
||||||
|
background: var(--color-bg-input);
|
||||||
|
box-shadow: 0 0 0 1px #3a3d5c inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .el-select__wrapper:hover {
|
||||||
|
box-shadow: 0 0 0 1px #4a4d6c inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .el-select__selected-item {
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .el-select__placeholder {
|
||||||
|
color: #afafaf;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .el-select-dropdown {
|
||||||
|
background: #27293d;
|
||||||
|
border-color: #3a3d5c;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .el-select-dropdown__item {
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .el-select-dropdown__item:hover {
|
||||||
|
background: #2a2a3e;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .el-select-dropdown__item.is-selected {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .el-select-dropdown__item.is-disabled {
|
||||||
|
color: #666666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tag */
|
||||||
|
html.dark .el-tag--info {
|
||||||
|
background: #27293d;
|
||||||
|
border-color: #3a3d5c;
|
||||||
|
color: #b0b0b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button */
|
||||||
|
html.dark .el-button--default {
|
||||||
|
background: #27293d;
|
||||||
|
border-color: #3a3d5c;
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .el-button--default:hover {
|
||||||
|
background: #2a2a3e;
|
||||||
|
border-color: #4a4d6c;
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card */
|
||||||
|
html.dark .el-card {
|
||||||
|
background: #1e1e2e;
|
||||||
|
border-color: #3a3d5c;
|
||||||
|
color: #b0b0b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .el-card__header {
|
||||||
|
border-bottom-color: #3a3d5c;
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table */
|
||||||
html.dark .el-table {
|
html.dark .el-table {
|
||||||
background-color: #27293d;
|
background-color: #1e1e2e;
|
||||||
color: #e5e7eb;
|
color: #e5e7eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,9 +157,56 @@ html.dark .el-table th {
|
|||||||
}
|
}
|
||||||
|
|
||||||
html.dark .el-table tr {
|
html.dark .el-table tr {
|
||||||
background-color: #27293d;
|
background-color: #1e1e2e;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.dark .el-table--striped .el-table__body tr.el-table__row--striped td {
|
html.dark .el-table--striped .el-table__body tr.el-table__row--striped td {
|
||||||
background-color: #1e1e2e;
|
background-color: #181825;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dialog */
|
||||||
|
html.dark .el-dialog {
|
||||||
|
background: #1e1e2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .el-dialog__title {
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Message */
|
||||||
|
html.dark .el-message {
|
||||||
|
background: #27293d;
|
||||||
|
border-color: #3a3d5c;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .el-message--success {
|
||||||
|
background: #1e3d2e;
|
||||||
|
border-color: #3d6b4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .el-message--warning {
|
||||||
|
background: #3d3020;
|
||||||
|
border-color: #6b5020;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .el-message--error {
|
||||||
|
background: #3d2027;
|
||||||
|
border-color: #5c2d2d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading */
|
||||||
|
html.dark .el-loading-mask {
|
||||||
|
background-color: rgba(30, 30, 46, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Overlay */
|
||||||
|
html.dark .el-overlay {
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tooltip */
|
||||||
|
html.dark .el-tooltip__popper {
|
||||||
|
background: #27293d !important;
|
||||||
|
border-color: #3a3d5c !important;
|
||||||
|
color: #e5e7eb !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
:root {
|
||||||
|
/* Text colors */
|
||||||
|
--color-text-primary: #303133;
|
||||||
|
--color-text-secondary: #606266;
|
||||||
|
--color-text-muted: #909399;
|
||||||
|
--color-text-light: #c0c4cc;
|
||||||
|
--color-text-placeholder: #a8abb2;
|
||||||
|
|
||||||
|
/* Background colors */
|
||||||
|
--color-bg-primary: #ffffff;
|
||||||
|
--color-bg-secondary: #f9f9f9;
|
||||||
|
--color-bg-tertiary: #fafafa;
|
||||||
|
--color-bg-surface: #ffffff;
|
||||||
|
--color-bg-muted: #f4f4f5;
|
||||||
|
--color-bg-input: #ffffff;
|
||||||
|
--color-bg-hover: #efefef;
|
||||||
|
--color-bg-active: #eaeaea;
|
||||||
|
|
||||||
|
/* Border colors */
|
||||||
|
--color-border: #dcdfe6;
|
||||||
|
--color-border-light: #e4e7ed;
|
||||||
|
--color-border-lighter: #ebeef5;
|
||||||
|
--color-border-extra-light: #f2f6fc;
|
||||||
|
|
||||||
|
/* Status colors */
|
||||||
|
--color-primary: #409eff;
|
||||||
|
--color-primary-light: #ecf5ff;
|
||||||
|
--color-success: #67c23a;
|
||||||
|
--color-warning: #e6a23c;
|
||||||
|
--color-danger: #f56c6c;
|
||||||
|
--color-danger-dark: #c45656;
|
||||||
|
--color-danger-light: #fef0f0;
|
||||||
|
--color-info: #909399;
|
||||||
|
|
||||||
|
/* Element Plus mapping */
|
||||||
|
--el-color-primary: var(--color-primary);
|
||||||
|
--el-color-success: var(--color-success);
|
||||||
|
--el-color-warning: var(--color-warning);
|
||||||
|
--el-color-danger: var(--color-danger);
|
||||||
|
--el-color-info: var(--color-info);
|
||||||
|
|
||||||
|
--el-text-color-primary: var(--color-text-primary);
|
||||||
|
--el-text-color-regular: var(--color-text-secondary);
|
||||||
|
--el-text-color-secondary: var(--color-text-muted);
|
||||||
|
--el-text-color-placeholder: var(--color-text-placeholder);
|
||||||
|
|
||||||
|
--el-bg-color: var(--color-bg-primary);
|
||||||
|
--el-bg-color-page: var(--color-bg-secondary);
|
||||||
|
--el-bg-color-overlay: var(--color-bg-primary);
|
||||||
|
|
||||||
|
--el-border-color: var(--color-border);
|
||||||
|
--el-border-color-light: var(--color-border-light);
|
||||||
|
--el-border-color-lighter: var(--color-border-lighter);
|
||||||
|
--el-border-color-extra-light: var(--color-border-extra-light);
|
||||||
|
|
||||||
|
--el-fill-color-blank: var(--color-bg-primary);
|
||||||
|
--el-fill-color-light: var(--color-bg-tertiary);
|
||||||
|
--el-fill-color: var(--color-bg-tertiary);
|
||||||
|
--el-fill-color-dark: var(--color-bg-hover);
|
||||||
|
--el-fill-color-darker: var(--color-bg-active);
|
||||||
|
|
||||||
|
/* Input */
|
||||||
|
--el-input-bg-color: var(--color-bg-input);
|
||||||
|
--el-input-border-color: var(--color-border);
|
||||||
|
--el-input-hover-border-color: var(--color-border-light);
|
||||||
|
|
||||||
|
/* Dialog */
|
||||||
|
--el-dialog-bg-color: var(--color-bg-primary);
|
||||||
|
--el-overlay-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
/* Text colors */
|
||||||
|
--color-text-primary: #e5e7eb;
|
||||||
|
--color-text-secondary: #b0b0b0;
|
||||||
|
--color-text-muted: #888888;
|
||||||
|
--color-text-light: #666666;
|
||||||
|
--color-text-placeholder: #afafaf;
|
||||||
|
|
||||||
|
/* Background colors */
|
||||||
|
--color-bg-primary: #1e1e2e;
|
||||||
|
--color-bg-secondary: #181825;
|
||||||
|
--color-bg-tertiary: #27293d;
|
||||||
|
--color-bg-surface: #27293d;
|
||||||
|
--color-bg-muted: #27293d;
|
||||||
|
--color-bg-input: #27293d;
|
||||||
|
--color-bg-hover: #2a2a3e;
|
||||||
|
--color-bg-active: #353550;
|
||||||
|
|
||||||
|
/* Border colors */
|
||||||
|
--color-border: #3a3d5c;
|
||||||
|
--color-border-light: #313348;
|
||||||
|
--color-border-lighter: #2a2a3e;
|
||||||
|
--color-border-extra-light: #222233;
|
||||||
|
|
||||||
|
/* Status colors */
|
||||||
|
--color-primary: #409eff;
|
||||||
|
--color-danger: #f87171;
|
||||||
|
--color-danger-dark: #f87171;
|
||||||
|
--color-danger-light: #3d2027;
|
||||||
|
--color-info: #888888;
|
||||||
|
|
||||||
|
/* Dark overrides */
|
||||||
|
--el-text-color-regular: var(--color-text-primary);
|
||||||
|
--el-overlay-color: rgba(0, 0, 0, 0.7);
|
||||||
|
|
||||||
|
background-color: #181825;
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { useBreakpoints } from '@vueuse/core'
|
||||||
|
|
||||||
|
const breakpoints = useBreakpoints({ mobile: 0, desktop: 768 })
|
||||||
|
|
||||||
|
export function useResponsive() {
|
||||||
|
const isMobile = breakpoints.smaller('desktop') // < 768px
|
||||||
|
return { isMobile }
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import 'element-plus/theme-chalk/dark/css-vars.css'
|
|||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
|
||||||
import './assets/css/custom.css'
|
import './assets/css/var.css'
|
||||||
import './assets/css/dark.css'
|
import './assets/css/dark.css'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|||||||
@@ -8,23 +8,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions-section">
|
<div class="actions-section">
|
||||||
<el-button :icon="Refresh" class="action-btn" @click="fetchData"
|
<ActionButton variant="outline" size="small" @click="fetchData">
|
||||||
>Refresh</el-button
|
Refresh
|
||||||
>
|
</ActionButton>
|
||||||
|
|
||||||
<el-popconfirm
|
<ActionButton variant="outline" size="small" danger @click="showClearDialog = true">
|
||||||
title="Clear all offline proxies?"
|
Clear Offline
|
||||||
width="220"
|
</ActionButton>
|
||||||
confirm-button-text="Clear"
|
|
||||||
cancel-button-text="Cancel"
|
|
||||||
@confirm="clearOfflineProxies"
|
|
||||||
>
|
|
||||||
<template #reference>
|
|
||||||
<el-button :icon="Delete" class="action-btn" type="danger" plain
|
|
||||||
>Clear Offline</el-button
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
</el-popconfirm>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -38,28 +28,35 @@
|
|||||||
class="main-search"
|
class="main-search"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<el-select
|
<PopoverMenu
|
||||||
:model-value="selectedClientKey"
|
:model-value="selectedClientKey"
|
||||||
placeholder="All Clients"
|
:width="220"
|
||||||
clearable
|
placement="bottom-end"
|
||||||
|
selectable
|
||||||
filterable
|
filterable
|
||||||
class="client-select"
|
filter-placeholder="Search clients..."
|
||||||
@change="onClientFilterChange"
|
:display-value="selectedClientLabel"
|
||||||
|
clearable
|
||||||
|
class="client-filter"
|
||||||
|
@update:model-value="onClientFilterChange($event as string)"
|
||||||
>
|
>
|
||||||
<el-option label="All Clients" value="" />
|
<template #default="{ filterText }">
|
||||||
<el-option
|
<PopoverMenuItem value="">All Clients</PopoverMenuItem>
|
||||||
v-if="clientIDFilter && !selectedClientInList"
|
<PopoverMenuItem
|
||||||
:label="`${userFilter ? userFilter + '.' : ''}${clientIDFilter} (not found)`"
|
v-if="clientIDFilter && !selectedClientInList"
|
||||||
:value="selectedClientKey"
|
:value="selectedClientKey"
|
||||||
style="color: var(--el-color-warning); font-style: italic"
|
>
|
||||||
/>
|
{{ userFilter ? userFilter + '.' : '' }}{{ clientIDFilter }} (not found)
|
||||||
<el-option
|
</PopoverMenuItem>
|
||||||
v-for="client in clientOptions"
|
<PopoverMenuItem
|
||||||
:key="client.key"
|
v-for="client in filteredClientOptions(filterText)"
|
||||||
:label="client.label"
|
:key="client.key"
|
||||||
:value="client.key"
|
:value="client.key"
|
||||||
/>
|
>
|
||||||
</el-select>
|
{{ client.label }}
|
||||||
|
</PopoverMenuItem>
|
||||||
|
</template>
|
||||||
|
</PopoverMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="type-tabs">
|
<div class="type-tabs">
|
||||||
@@ -88,6 +85,15 @@
|
|||||||
<el-empty description="No proxies found" />
|
<el-empty description="No proxies found" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
v-model="showClearDialog"
|
||||||
|
title="Clear Offline"
|
||||||
|
message="Are you sure you want to clear all offline proxies?"
|
||||||
|
confirm-text="Clear"
|
||||||
|
danger
|
||||||
|
@confirm="handleClearConfirm"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -95,7 +101,9 @@
|
|||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { Search, Refresh, Delete } from '@element-plus/icons-vue'
|
import { Search } from '@element-plus/icons-vue'
|
||||||
|
import ActionButton from '@shared/components/ActionButton.vue'
|
||||||
|
import ConfirmDialog from '@shared/components/ConfirmDialog.vue'
|
||||||
import {
|
import {
|
||||||
BaseProxy,
|
BaseProxy,
|
||||||
TCPProxy,
|
TCPProxy,
|
||||||
@@ -107,6 +115,8 @@ import {
|
|||||||
SUDPProxy,
|
SUDPProxy,
|
||||||
} from '../utils/proxy'
|
} from '../utils/proxy'
|
||||||
import ProxyCard from '../components/ProxyCard.vue'
|
import ProxyCard from '../components/ProxyCard.vue'
|
||||||
|
import PopoverMenu from '@shared/components/PopoverMenu.vue'
|
||||||
|
import PopoverMenuItem from '@shared/components/PopoverMenuItem.vue'
|
||||||
import {
|
import {
|
||||||
getProxiesByType,
|
getProxiesByType,
|
||||||
clearOfflineProxies as apiClearOfflineProxies,
|
clearOfflineProxies as apiClearOfflineProxies,
|
||||||
@@ -133,6 +143,7 @@ const proxies = ref<BaseProxy[]>([])
|
|||||||
const clients = ref<Client[]>([])
|
const clients = ref<Client[]>([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const searchText = ref('')
|
const searchText = ref('')
|
||||||
|
const showClearDialog = ref(false)
|
||||||
const clientIDFilter = ref((route.query.clientID as string) || '')
|
const clientIDFilter = ref((route.query.clientID as string) || '')
|
||||||
const userFilter = ref((route.query.user as string) || '')
|
const userFilter = ref((route.query.user as string) || '')
|
||||||
|
|
||||||
@@ -157,6 +168,20 @@ const selectedClientKey = computed(() => {
|
|||||||
return client?.key || `${userFilter.value}:${clientIDFilter.value}`
|
return client?.key || `${userFilter.value}:${clientIDFilter.value}`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const selectedClientLabel = computed(() => {
|
||||||
|
if (!clientIDFilter.value) return 'All Clients'
|
||||||
|
const client = clientOptions.value.find(
|
||||||
|
(c) => c.clientID === clientIDFilter.value && c.user === userFilter.value,
|
||||||
|
)
|
||||||
|
return client?.label || `${userFilter.value ? userFilter.value + '.' : ''}${clientIDFilter.value}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredClientOptions = (filterText: string) => {
|
||||||
|
if (!filterText) return clientOptions.value
|
||||||
|
const search = filterText.toLowerCase()
|
||||||
|
return clientOptions.value.filter((c) => c.label.toLowerCase().includes(search))
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the filtered client exists in the client list
|
// Check if the filtered client exists in the client list
|
||||||
const selectedClientInList = computed(() => {
|
const selectedClientInList = computed(() => {
|
||||||
if (!clientIDFilter.value) return true
|
if (!clientIDFilter.value) return true
|
||||||
@@ -275,6 +300,11 @@ const fetchData = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleClearConfirm = async () => {
|
||||||
|
showClearDialog.value = false
|
||||||
|
await clearOfflineProxies()
|
||||||
|
}
|
||||||
|
|
||||||
const clearOfflineProxies = async () => {
|
const clearOfflineProxies = async () => {
|
||||||
try {
|
try {
|
||||||
await apiClearOfflineProxies()
|
await apiClearOfflineProxies()
|
||||||
@@ -357,12 +387,6 @@ fetchClients()
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn {
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 8px 16px;
|
|
||||||
height: 36px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-section {
|
.filter-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -382,37 +406,16 @@ fetchClients()
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-search,
|
|
||||||
.client-select {
|
|
||||||
height: 44px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-search :deep(.el-input__wrapper),
|
.main-search :deep(.el-input__wrapper),
|
||||||
.client-select :deep(.el-input__wrapper) {
|
.client-filter :deep(.el-input__wrapper) {
|
||||||
border-radius: 12px;
|
height: 32px;
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
border-radius: 8px;
|
||||||
padding: 0 16px;
|
|
||||||
height: 100%;
|
|
||||||
border: 1px solid var(--el-border-color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-search :deep(.el-input__wrapper) {
|
.client-filter {
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-select {
|
|
||||||
width: 240px;
|
width: 240px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-select :deep(.el-select__wrapper) {
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
|
||||||
padding: 0 12px;
|
|
||||||
height: 44px;
|
|
||||||
min-height: 44px;
|
|
||||||
border: 1px solid var(--el-border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.type-tabs {
|
.type-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -462,7 +465,7 @@ fetchClients()
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-select {
|
.client-filter {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,98 +51,41 @@
|
|||||||
<router-link
|
<router-link
|
||||||
v-if="proxy.clientID"
|
v-if="proxy.clientID"
|
||||||
:to="clientLink"
|
:to="clientLink"
|
||||||
class="client-link"
|
class="meta-link"
|
||||||
>
|
>
|
||||||
<el-icon><Monitor /></el-icon>
|
<el-icon><Monitor /></el-icon>
|
||||||
<span
|
<span>{{
|
||||||
>Client:
|
proxy.user
|
||||||
{{
|
? `${proxy.user}.${proxy.clientID}`
|
||||||
proxy.user
|
: proxy.clientID
|
||||||
? `${proxy.user}.${proxy.clientID}`
|
}}</span>
|
||||||
: proxy.clientID
|
|
||||||
}}</span
|
|
||||||
>
|
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<span v-if="proxy.lastStartTime" class="meta-text">
|
||||||
|
<span class="meta-sep">·</span>
|
||||||
|
Last Started {{ proxy.lastStartTime }}
|
||||||
|
</span>
|
||||||
|
<span v-if="proxy.lastCloseTime" class="meta-text">
|
||||||
|
<span class="meta-sep">·</span>
|
||||||
|
Last Closed {{ proxy.lastCloseTime }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stats Cards -->
|
<!-- Stats Bar -->
|
||||||
<div class="stats-grid">
|
<div class="stats-bar">
|
||||||
<div v-if="proxy.port" class="stat-card">
|
<div v-if="proxy.port" class="stats-item">
|
||||||
<div class="stat-header">
|
<span class="stats-label">Port</span>
|
||||||
<span class="stat-label">Port</span>
|
<span class="stats-value">{{ proxy.port }}</span>
|
||||||
<div class="stat-icon port">
|
|
||||||
<el-icon><Connection /></el-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-value">{{ proxy.port }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stats-item">
|
||||||
<div class="stat-header">
|
<span class="stats-label">Connections</span>
|
||||||
<span class="stat-label">Connections</span>
|
<span class="stats-value">{{ proxy.conns }}</span>
|
||||||
<div class="stat-icon connections">
|
|
||||||
<el-icon><DataLine /></el-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-value">{{ proxy.conns }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stats-item">
|
||||||
<div class="stat-header">
|
<span class="stats-label">Traffic</span>
|
||||||
<span class="stat-label">Traffic In</span>
|
<span class="stats-value">↓ {{ formatTrafficValue(proxy.trafficIn) }} <small>{{ formatTrafficUnit(proxy.trafficIn) }}</small> / ↑ {{ formatTrafficValue(proxy.trafficOut) }} <small>{{ formatTrafficUnit(proxy.trafficOut) }}</small></span>
|
||||||
<div class="stat-icon traffic-in">
|
|
||||||
<el-icon><Bottom /></el-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-value">
|
|
||||||
<span class="value-number">{{
|
|
||||||
formatTrafficValue(proxy.trafficIn)
|
|
||||||
}}</span>
|
|
||||||
<span class="value-unit">{{
|
|
||||||
formatTrafficUnit(proxy.trafficIn)
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-header">
|
|
||||||
<span class="stat-label">Traffic Out</span>
|
|
||||||
<div class="stat-icon traffic-out">
|
|
||||||
<el-icon><Top /></el-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-value">
|
|
||||||
<span class="value-number">{{
|
|
||||||
formatTrafficValue(proxy.trafficOut)
|
|
||||||
}}</span>
|
|
||||||
<span class="value-unit">{{
|
|
||||||
formatTrafficUnit(proxy.trafficOut)
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Status Timeline -->
|
|
||||||
<div class="timeline-card">
|
|
||||||
<div class="timeline-header">
|
|
||||||
<el-icon><DataLine /></el-icon>
|
|
||||||
<h2>Status Timeline</h2>
|
|
||||||
</div>
|
|
||||||
<div class="timeline-body">
|
|
||||||
<div class="timeline-grid">
|
|
||||||
<div class="timeline-item">
|
|
||||||
<span class="timeline-label">Last Start Time</span>
|
|
||||||
<span class="timeline-value">{{
|
|
||||||
proxy.lastStartTime || '-'
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
<div class="timeline-item">
|
|
||||||
<span class="timeline-label">Last Close Time</span>
|
|
||||||
<span class="timeline-value">{{
|
|
||||||
proxy.lastCloseTime || '-'
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -288,9 +231,6 @@ import {
|
|||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
Monitor,
|
Monitor,
|
||||||
Connection,
|
Connection,
|
||||||
DataLine,
|
|
||||||
Bottom,
|
|
||||||
Top,
|
|
||||||
Link,
|
Link,
|
||||||
Lock,
|
Lock,
|
||||||
Promotion,
|
Promotion,
|
||||||
@@ -593,177 +533,72 @@ html.dark .status-badge.online {
|
|||||||
.header-meta {
|
.header-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-link {
|
.meta-link {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 4px;
|
||||||
font-size: 14px;
|
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: color 0.2s;
|
transition: color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-link:hover {
|
.meta-link:hover {
|
||||||
color: var(--el-color-primary);
|
color: var(--el-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Stats Grid */
|
.meta-text {
|
||||||
.stats-grid {
|
color: var(--text-muted);
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card {
|
.meta-sep {
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats Bar */
|
||||||
|
.stats-bar {
|
||||||
|
display: flex;
|
||||||
background: var(--el-bg-color);
|
background: var(--el-bg-color);
|
||||||
border: 1px solid var(--header-border);
|
border: 1px solid var(--header-border);
|
||||||
border-radius: 12px;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-label {
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-icon {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
display: flex;
|
margin-bottom: 20px;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-icon.port {
|
.stats-item {
|
||||||
background: rgba(139, 92, 246, 0.1);
|
flex: 1;
|
||||||
color: #8b5cf6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-icon.connections {
|
|
||||||
background: rgba(168, 85, 247, 0.1);
|
|
||||||
color: #a855f7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-icon.traffic-in {
|
|
||||||
background: rgba(59, 130, 246, 0.1);
|
|
||||||
color: #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-icon.traffic-out {
|
|
||||||
background: rgba(34, 197, 94, 0.1);
|
|
||||||
color: #22c55e;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark .stat-icon.port {
|
|
||||||
background: rgba(139, 92, 246, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark .stat-icon.connections {
|
|
||||||
background: rgba(168, 85, 247, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark .stat-icon.traffic-in {
|
|
||||||
background: rgba(59, 130, 246, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark .stat-icon.traffic-out {
|
|
||||||
background: rgba(34, 197, 94, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-value {
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value-number {
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-primary);
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-value:not(:has(.value-number)) {
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.value-unit {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Timeline Card */
|
|
||||||
.timeline-card {
|
|
||||||
background: var(--el-bg-color);
|
|
||||||
border: 1px solid var(--header-border);
|
|
||||||
border-radius: 12px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 16px 20px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-header h2 {
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-body {
|
|
||||||
padding: 20px;
|
|
||||||
padding-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: 24px;
|
|
||||||
background: var(--el-fill-color-light);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 20px 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-item {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 4px;
|
||||||
|
padding: 16px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-label {
|
.stats-item + .stats-item {
|
||||||
font-size: 13px;
|
border-left: 1px solid var(--header-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-label {
|
||||||
|
font-size: 12px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-value {
|
.stats-value {
|
||||||
font-size: 15px;
|
font-size: 18px;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stats-value small {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Card Base */
|
/* Card Base */
|
||||||
.traffic-card {
|
.traffic-card {
|
||||||
background: var(--el-bg-color);
|
background: var(--el-bg-color);
|
||||||
@@ -956,16 +791,22 @@ html.dark .config-item-icon.route {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive */
|
/* Responsive */
|
||||||
@media (max-width: 1024px) {
|
|
||||||
.stats-grid {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.config-grid {
|
.config-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stats-bar {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-item {
|
||||||
|
flex: 1 1 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-item:nth-child(n+3) {
|
||||||
|
border-top: 1px solid var(--header-border);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
@@ -974,12 +815,5 @@ html.dark .config-item-icon.route {
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -18,8 +18,12 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noFallthroughCasesInSwitch": true
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"],
|
||||||
|
"@shared/*": ["../shared/*"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "../shared/**/*.ts", "../shared/**/*.vue"],
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,20 @@ export default defineConfig({
|
|||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
'@shared': fileURLToPath(new URL('../shared', import.meta.url)),
|
||||||
|
},
|
||||||
|
dedupe: ['vue', 'element-plus', '@element-plus/icons-vue'],
|
||||||
|
modules: [
|
||||||
|
fileURLToPath(new URL('../node_modules', import.meta.url)),
|
||||||
|
'node_modules',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
preprocessorOptions: {
|
||||||
|
scss: {
|
||||||
|
api: 'modern',
|
||||||
|
additionalData: `@use "@shared/css/_index.scss" as *;`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
|||||||
+554
-503
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "frp-web",
|
||||||
|
"private": true,
|
||||||
|
"workspaces": ["shared", "frpc", "frps"]
|
||||||
|
}
|
||||||
@@ -21,7 +21,6 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useResponsive } from '../composables/useResponsive'
|
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -33,6 +32,7 @@ const props = withDefaults(
|
|||||||
closeOnPressEscape?: boolean
|
closeOnPressEscape?: boolean
|
||||||
appendToBody?: boolean
|
appendToBody?: boolean
|
||||||
top?: string
|
top?: string
|
||||||
|
isMobile?: boolean
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
width: '480px',
|
width: '480px',
|
||||||
@@ -41,6 +41,7 @@ const props = withDefaults(
|
|||||||
closeOnPressEscape: true,
|
closeOnPressEscape: true,
|
||||||
appendToBody: false,
|
appendToBody: false,
|
||||||
top: '15vh',
|
top: '15vh',
|
||||||
|
isMobile: false,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -53,15 +54,13 @@ const visible = computed({
|
|||||||
set: (value) => emit('update:modelValue', value),
|
set: (value) => emit('update:modelValue', value),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { isMobile } = useResponsive()
|
|
||||||
|
|
||||||
const dialogWidth = computed(() => {
|
const dialogWidth = computed(() => {
|
||||||
if (isMobile.value) return '100%'
|
if (props.isMobile) return '100%'
|
||||||
return props.width
|
return props.width
|
||||||
})
|
})
|
||||||
|
|
||||||
const dialogTop = computed(() => {
|
const dialogTop = computed(() => {
|
||||||
if (isMobile.value) return '0'
|
if (props.isMobile) return '0'
|
||||||
return props.top
|
return props.top
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
width="400px"
|
width="400px"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
:append-to-body="true"
|
:append-to-body="true"
|
||||||
|
:is-mobile="isMobile"
|
||||||
>
|
>
|
||||||
<p class="confirm-message">{{ message }}</p>
|
<p class="confirm-message">{{ message }}</p>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -27,7 +28,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import BaseDialog from './BaseDialog.vue'
|
import BaseDialog from './BaseDialog.vue'
|
||||||
import ActionButton from './ActionButton.vue'
|
import ActionButton from '@shared/components/ActionButton.vue'
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -38,12 +39,14 @@ const props = withDefaults(
|
|||||||
cancelText?: string
|
cancelText?: string
|
||||||
danger?: boolean
|
danger?: boolean
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
|
isMobile?: boolean
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
confirmText: 'Confirm',
|
confirmText: 'Confirm',
|
||||||
cancelText: 'Cancel',
|
cancelText: 'Cancel',
|
||||||
danger: false,
|
danger: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
isMobile: false,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
+1
-3
@@ -30,9 +30,6 @@ import { computed } from 'vue'
|
|||||||
import { ArrowDown } from '@element-plus/icons-vue'
|
import { ArrowDown } from '@element-plus/icons-vue'
|
||||||
import PopoverMenu from './PopoverMenu.vue'
|
import PopoverMenu from './PopoverMenu.vue'
|
||||||
import PopoverMenuItem from './PopoverMenuItem.vue'
|
import PopoverMenuItem from './PopoverMenuItem.vue'
|
||||||
import { useResponsive } from '../composables/useResponsive'
|
|
||||||
|
|
||||||
const { isMobile } = useResponsive()
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: string
|
modelValue: string
|
||||||
@@ -41,6 +38,7 @@ interface Props {
|
|||||||
allLabel?: string
|
allLabel?: string
|
||||||
width?: number
|
width?: number
|
||||||
minWidth?: number
|
minWidth?: number
|
||||||
|
isMobile?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"name": "frp-shared",
|
||||||
|
"private": true
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user