Compare commits
128 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
166f25508c | ||
|
08f0c25dfc | ||
|
6cca88f027 | ||
|
f62934440d | ||
|
44aded493e | ||
|
8b7d6be398 | ||
|
a0878876b2 | ||
|
d62d0aa11c | ||
|
b78c92b2df | ||
|
037a131b66 | ||
|
970c0d0430 | ||
|
fb99e88487 | ||
|
1a827e04f4 | ||
|
1f663e1fd5 | ||
|
6606956d68 | ||
|
ade07f7450 | ||
|
ab4d896efc | ||
|
c2b3d19a51 | ||
|
de57fbd262 | ||
|
1772de7c5e | ||
|
e60db34091 | ||
|
6ae78b13a2 | ||
|
3cce7ebf01 | ||
|
9d45bf6601 | ||
|
22ab32052e | ||
|
19e6344a81 | ||
|
e90941f52b | ||
|
07989705d2 | ||
|
45f11483ec | ||
|
0552b69b36 | ||
|
f853cd9ec9 | ||
|
238ec25314 | ||
|
626a7d6059 | ||
|
da3bac31a2 | ||
|
a590d3a56e | ||
|
885330e948 | ||
|
22738b6244 | ||
|
cd42b1b9d5 | ||
|
867f840265 | ||
|
e7679a3fa6 | ||
|
68f6da436d | ||
|
58cc8aa5df | ||
|
17b02640a9 | ||
|
fc98c0919f | ||
|
d699d0ee87 | ||
|
cde2ef3bd7 | ||
|
bf71ff4cff | ||
|
dc14a357b8 | ||
|
d9102006f5 | ||
|
1bef03e036 | ||
|
9905390ba6 | ||
|
f6ecd26607 | ||
|
a1bac6e525 | ||
|
3efbf9cce1 | ||
|
be79b11e58 | ||
|
8eec79d54f | ||
|
288e1b75e9 | ||
|
8640cbbf42 | ||
|
3a09ec94e8 | ||
|
3168055948 | ||
|
e15dddae73 | ||
|
6322c22b49 | ||
|
94e7e47375 | ||
|
49f05c1fc6 | ||
|
15cd1d8c85 | ||
|
05c5bcc6cb | ||
|
40c811c143 | ||
|
d7012e36bf | ||
|
ca997cc994 | ||
|
5114fc2854 | ||
|
482d787a56 | ||
|
6eb28d03b3 | ||
|
29e2e2f7b2 | ||
|
ae18401351 | ||
|
175215698d | ||
|
3b23717080 | ||
|
45fcbd029f | ||
|
1e3c6b4a66 | ||
|
417f99d94f | ||
|
ff0ff9a9ee | ||
|
b4edb4cc95 | ||
|
68d7e16773 | ||
|
ebc4fad9bc | ||
|
8c452bdaa9 | ||
|
380973a200 | ||
|
d086e98711 | ||
|
fa9d02396b | ||
|
eae12d8df2 | ||
|
d4be2a0bcf | ||
|
33ec746a54 | ||
|
0b6d0977ac | ||
|
7282882269 | ||
|
d16c1ed3c2 | ||
|
79682034e2 | ||
|
5cf8b8024c | ||
|
31520debb2 | ||
|
c632d41702 | ||
|
7c784b31b7 | ||
|
6b650169df | ||
|
61e755d2b9 | ||
|
e792215f6e | ||
|
c9ec634857 | ||
|
4635bcf44f | ||
|
bdffd2d840 | ||
|
8d0f6fb7c4 | ||
|
cb9436a8f7 | ||
|
c4011934f7 | ||
|
f99edfd7bd | ||
|
387da11f1b | ||
|
73e3e4f3a1 | ||
|
65aa18300d | ||
|
15585ef12e | ||
|
7e6864fa8c | ||
|
ae4fd35a01 | ||
|
9c08ebe956 | ||
|
5efd995992 | ||
|
aa0d570b2b | ||
|
cf5408751e | ||
|
aa20c97312 | ||
|
d3f907b5c3 | ||
|
53f1abb7c8 | ||
|
41391989e7 | ||
|
7fb015b07d | ||
|
606e10ca0a | ||
|
4fc0aad7a0 | ||
|
843ecdaa33 | ||
|
6cd243ea40 | ||
|
9e6bbd3386 |
BIN
.github/admin-frontend.20241128.png
vendored
Normal file
BIN
.github/admin-frontend.20241128.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
2
.github/sync.py
vendored
2
.github/sync.py
vendored
@ -7,7 +7,7 @@ from github import Github
|
||||
|
||||
def get_github_latest_release():
|
||||
g = Github()
|
||||
repo = g.get_repo("naiba/nezha")
|
||||
repo = g.get_repo("nezhahq/nezha")
|
||||
release = repo.get_latest_release()
|
||||
if release:
|
||||
print(f"Latest release tag is: {release.tag_name}")
|
||||
|
BIN
.github/user-frontend.20241128.png
vendored
Normal file
BIN
.github/user-frontend.20241128.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 53 KiB |
112
.github/workflows/release.yml
vendored
112
.github/workflows/release.yml
vendored
@ -4,6 +4,8 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
branches:
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@ -17,23 +19,43 @@ jobs:
|
||||
goarch: s390x
|
||||
- goos: linux
|
||||
goarch: arm64
|
||||
|
||||
|
||||
name: Build artifacts
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: goreleaser/goreleaser-cross:v1.23
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOARM: ${{ matrix.goarm }}
|
||||
outputs:
|
||||
tag_name: ${{ steps.extract_branch.outputs.tag }}
|
||||
steps:
|
||||
- run: git config --global --add safe.directory /__w/nezha/nezha
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: robinraju/release-downloader@v1
|
||||
with:
|
||||
repository: nezhahq/admin-frontend
|
||||
tag: v1.0.11
|
||||
fileName: dist.zip
|
||||
latest: true
|
||||
extract: true
|
||||
|
||||
- name: prepare admin-frontend dists
|
||||
run: |
|
||||
rm -rf cmd/dashboard/admin-dist
|
||||
mv dist cmd/dashboard/admin-dist
|
||||
|
||||
- uses: robinraju/release-downloader@v1
|
||||
with:
|
||||
repository: nezhahq/user-frontend
|
||||
tag: v1.0.4
|
||||
fileName: dist.zip
|
||||
latest: true
|
||||
extract: true
|
||||
|
||||
- name: prepare admin-frontend dists
|
||||
run: |
|
||||
rm -rf cmd/dashboard/user-dist
|
||||
mv dist cmd/dashboard/user-dist
|
||||
|
||||
- name: Fetch IPInfo GeoIP Database
|
||||
if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
|
||||
env:
|
||||
IPINFO_TOKEN: ${{ secrets.IPINFO_TOKEN }}
|
||||
run: |
|
||||
@ -44,18 +66,40 @@ jobs:
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.23.x"
|
||||
|
||||
- name: Build
|
||||
|
||||
- name: generate swagger docs
|
||||
run: |
|
||||
go install github.com/swaggo/swag/cmd/swag@latest
|
||||
swag init --pd -d . -g ./cmd/dashboard/main.go -o ./cmd/dashboard/docs --parseGoList=false
|
||||
|
||||
- name: Build with tag
|
||||
if: contains(github.ref, 'refs/tags/')
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOARM: ${{ matrix.goarm }}
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: '~> v2'
|
||||
version: "~> v2"
|
||||
args: build --single-target --clean --skip=validate
|
||||
|
||||
- name: Build snapshot
|
||||
if: contains(github.ref, 'refs/tags/') == false
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOARM: ${{ matrix.goarm }}
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: "~> v2"
|
||||
args: build --single-target --clean --skip=validate --snapshot
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dashboard-${{ env.GOOS }}-${{ env.GOARCH }}
|
||||
name: dashboard-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: |
|
||||
./dist/*/*
|
||||
|
||||
@ -72,12 +116,11 @@ jobs:
|
||||
|
||||
- name: Archive and compress binaries
|
||||
run: |
|
||||
for file in assets/*/*/*; do
|
||||
if [ -f "$file" ]; then
|
||||
chmod +x "$file"
|
||||
export fileWithoutExt=${file%.*}
|
||||
zip -jr "$fileWithoutExt.zip" "$file"
|
||||
fi
|
||||
find assets/*/*/* -type f | while read -r file; do
|
||||
dir=$(dirname "$file")
|
||||
filename=$(basename "$file")
|
||||
fileWithoutExt="${filename%.*}"
|
||||
zip -jr "$dir/$fileWithoutExt.zip" "$file"
|
||||
done
|
||||
|
||||
- name: Release
|
||||
@ -108,12 +151,12 @@ jobs:
|
||||
|
||||
release-docker:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
|
||||
if: github.event_name == 'push'
|
||||
needs: build
|
||||
name: Release Docker images
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
@ -125,12 +168,16 @@ jobs:
|
||||
mkdir dist
|
||||
mv ./assets/*/*/* ./dist
|
||||
|
||||
- name: Extract branch name
|
||||
- name: Extract branch name in tag
|
||||
run: |
|
||||
export TAG_NAME=$(echo ${GITHUB_REF#refs/tags/})
|
||||
if [[ $GITHUB_REF == refs/heads/* ]]; then
|
||||
export TAG_NAME=$(echo ${GITHUB_REF#refs/heads/})
|
||||
else
|
||||
export TAG_NAME=$(echo ${GITHUB_REF#refs/tags/})
|
||||
fi
|
||||
echo "tag=$TAG_NAME" >> $GITHUB_OUTPUT
|
||||
id: extract_branch
|
||||
|
||||
|
||||
- name: Log into GHCR
|
||||
uses: docker/login-action@master
|
||||
with:
|
||||
@ -139,6 +186,7 @@ jobs:
|
||||
password: ${{ github.token }}
|
||||
|
||||
- name: Log in to the AliyunCS
|
||||
if: contains(github.ref, 'refs/tags/')
|
||||
uses: docker/login-action@master
|
||||
with:
|
||||
registry: registry.cn-shanghai.aliyuncs.com
|
||||
@ -153,8 +201,8 @@ jobs:
|
||||
|
||||
- name: Set up image name
|
||||
run: |
|
||||
GHCR_IMAGE_NAME=$(echo "ghcr.io/${{ github.repository_owner }}/nezha-dashboard" | tr '[:upper:]' '[:lower:]')
|
||||
if [ ${{ github.repository_owner }} = "naiba" ]
|
||||
GHCR_IMAGE_NAME=$(echo "ghcr.io/${{ github.repository_owner }}/nezha" | tr '[:upper:]' '[:lower:]')
|
||||
if [ ${{ github.repository_owner }} = "nezhahq" ]
|
||||
then ALI_IMAGE_NAME=$(echo "registry.cn-shanghai.aliyuncs.com/naibahq/nezha-dashboard")
|
||||
else ALI_IMAGE_NAME=$(echo "registry.cn-shanghai.aliyuncs.com/${{ github.repository_owner }}/nezha-dashboard" | tr '[:upper:]' '[:lower:]')
|
||||
fi
|
||||
@ -162,7 +210,8 @@ jobs:
|
||||
echo "ALI_IMAGE_NAME=$ALI_IMAGE_NAME" >> $GITHUB_OUTPUT
|
||||
id: image-name
|
||||
|
||||
- name: Build dasbboard image And Push
|
||||
- name: Build dasbboard image And Push with tag
|
||||
if: contains(github.ref, 'refs/tags/')
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
@ -174,3 +223,14 @@ jobs:
|
||||
${{ steps.image-name.outputs.GHCR_IMAGE_NAME }}:${{ steps.extract_branch.outputs.tag }}
|
||||
${{ steps.image-name.outputs.ALI_IMAGE_NAME }}:latest
|
||||
${{ steps.image-name.outputs.ALI_IMAGE_NAME }}:${{ steps.extract_branch.outputs.tag }}
|
||||
|
||||
- name: Build dasbboard image And Push snapshot
|
||||
if: contains(github.ref, 'refs/tags/') == false
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/s390x
|
||||
push: true
|
||||
tags: |
|
||||
${{ steps.image-name.outputs.GHCR_IMAGE_NAME }}:${{ steps.extract_branch.outputs.tag }}
|
||||
|
2
.github/workflows/sync-code.yml
vendored
2
.github/workflows/sync-code.yml
vendored
@ -13,4 +13,4 @@ jobs:
|
||||
with:
|
||||
destination_repository: git@gitee.com:naibahq/nezha.git
|
||||
destination_branch_name: master
|
||||
destination_ssh_key: ${{ secrets.GITLAB_SSH_KEY }}
|
||||
destination_ssh_key: ${{ secrets.GITEE_SSH_KEY }}
|
||||
|
1
.github/workflows/sync-release.yml
vendored
1
.github/workflows/sync-release.yml
vendored
@ -6,6 +6,7 @@ on:
|
||||
jobs:
|
||||
sync-release-to-gitee:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }}
|
||||
steps:
|
||||
|
9
.github/workflows/test.yml
vendored
9
.github/workflows/test.yml
vendored
@ -2,8 +2,6 @@ name: Run Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "**.go"
|
||||
- "go.mod"
|
||||
@ -30,6 +28,13 @@ jobs:
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.23.x"
|
||||
|
||||
- name: generate swagger docs
|
||||
run: |
|
||||
go install github.com/swaggo/swag/cmd/swag@latest
|
||||
touch ./cmd/dashboard/user-dist/a
|
||||
touch ./cmd/dashboard/admin-dist/a
|
||||
swag init --pd -d . -g ./cmd/dashboard/main.go -o ./cmd/dashboard/docs --parseGoList=false
|
||||
|
||||
- name: Unit test
|
||||
run: |
|
||||
|
9
.gitignore
vendored
9
.gitignore
vendored
@ -9,14 +9,19 @@
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*~
|
||||
*.out
|
||||
*.pprof
|
||||
.idea
|
||||
/data
|
||||
/dist
|
||||
.DS_Store
|
||||
/main
|
||||
/cmd/dashboard/data
|
||||
/cmd/dashboard/main
|
||||
/cmd/dashboard/admin-dist/*
|
||||
/cmd/dashboard/user-dist/*
|
||||
!/cmd/dashboard/admin-dist/.gitkeep
|
||||
!/cmd/dashboard/user-dist/.gitkeep
|
||||
/config.yml
|
||||
/resource/template/theme-custom
|
||||
/resource/static/custom
|
||||
/cmd/dashboard/docs
|
||||
|
@ -9,10 +9,11 @@ builds:
|
||||
- CC=aarch64-linux-gnu-gcc
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X github.com/naiba/nezha/service/singleton.Version={{.Version}}
|
||||
- -X github.com/nezhahq/nezha/service/singleton.Version={{.Version}}
|
||||
- -extldflags "-static -fpic"
|
||||
flags:
|
||||
- -trimpath
|
||||
- -buildvcs=false
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
@ -25,10 +26,11 @@ builds:
|
||||
- CC=x86_64-linux-gnu-gcc
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X github.com/naiba/nezha/service/singleton.Version={{.Version}}
|
||||
- -X github.com/nezhahq/nezha/service/singleton.Version={{.Version}}
|
||||
- -extldflags "-static -fpic"
|
||||
flags:
|
||||
- -trimpath
|
||||
- -buildvcs=false
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
@ -41,10 +43,11 @@ builds:
|
||||
- CC=s390x-linux-gnu-gcc
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X github.com/naiba/nezha/service/singleton.Version={{.Version}}
|
||||
- -X github.com/nezhahq/nezha/service/singleton.Version={{.Version}}
|
||||
- -extldflags "-static -fpic"
|
||||
flags:
|
||||
- -trimpath
|
||||
- -buildvcs=false
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
@ -57,10 +60,11 @@ builds:
|
||||
- CC=x86_64-w64-mingw32-gcc
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X github.com/naiba/nezha/service/singleton.Version={{.Version}}
|
||||
- -X github.com/nezhahq/nezha/service/singleton.Version={{.Version}}
|
||||
- -extldflags "-static -fpic"
|
||||
flags:
|
||||
- -trimpath
|
||||
- -buildvcs=false
|
||||
goos:
|
||||
- windows
|
||||
goarch:
|
||||
|
18
README.md
18
README.md
@ -1,10 +1,10 @@
|
||||
<div align="center">
|
||||
<br>
|
||||
<img width="360" style="max-width:80%" src="resource/static/brand.svg" title="哪吒监控 Nezha Monitoring">
|
||||
<img width="360" style="max-width:80%" src=".github/brand.svg" title="哪吒监控 Nezha Monitoring">
|
||||
<br>
|
||||
<small><i>LOGO designed by <a href="https://xio.ng" target="_blank">熊大</a> .</i></small>
|
||||
<br><br>
|
||||
<img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/naiba/nezha?color=brightgreen&style=for-the-badge&logo=github&label=Dashboard"> <img src="https://img.shields.io/github/v/release/nezhahq/agent?color=brightgreen&label=Agent&style=for-the-badge&logo=github"> <img src="https://img.shields.io/github/actions/workflow/status/nezhahq/agent/agent.yml?label=Agent%20CI&logo=github&style=for-the-badge"> <img src="https://img.shields.io/badge/Installer-v0.20.2-brightgreen?style=for-the-badge&logo=linux">
|
||||
<img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/nezhahq/nezha?color=brightgreen&style=for-the-badge&logo=github&label=Dashboard"> <img src="https://img.shields.io/github/v/release/nezhahq/agent?color=brightgreen&label=Agent&style=for-the-badge&logo=github"> <img src="https://img.shields.io/github/actions/workflow/status/nezhahq/agent/agent.yml?label=Agent%20CI&logo=github&style=for-the-badge"> <img src="https://img.shields.io/badge/Installer-v0.20.2-brightgreen?style=for-the-badge&logo=linux">
|
||||
<br>
|
||||
<br>
|
||||
<p>:trollface: <b>Nezha Monitoring: Self-hostable, lightweight, servers and websites monitoring and O&M tool.</b></p>
|
||||
@ -24,11 +24,10 @@
|
||||
|
||||
## Screenshots
|
||||
|
||||
| Default Theme | DayNight [@JackieSung](https://github.com/JackieSung4ev) | hotaru |
|
||||
| ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- |
|
||||
|  | <img src="resource/template/theme-daynight/screenshot.png" width="3000px"/> | <img src="resource/template/theme-hotaru/screenshot.png" width="1500px" /> |
|
||||
| <div align="center"><b>Neko Mdui <a href="https://github.com/MikoyChinese">@MikoyChinese</a></b></div> | <div align="center"><b>AngelKanade <a href="https://github.com/adminsama">@adminsama</a></b></div> |<div align="center"><b>ServerStatus <a href="https://github.com/unclezs">@unclezs</a></b></div> |
|
||||
|  |  |  |
|
||||
| 用户前台 [@hamster1963](https://github.com/hamster1963) | 管理后台 [@uubulb](https://github.com/uubulb) |
|
||||
|---|---|
|
||||
|  |  |
|
||||
| [hamster1963/nezha-dash-react](https://github.com/hamster1963/nezha-dash-react) | [nezhahq/admin-frontend](https://github.com/nezhahq/admin-frontend) |
|
||||
|
||||
## Supported Languages
|
||||
|
||||
@ -41,9 +40,9 @@ You can change the dashboard language in the settings page (`/setting`) after th
|
||||
## Contributors
|
||||
|
||||
<!--GAMFC_DELIMITER--><a href="https://github.com/naiba" title="naiba"><img src="https://avatars.githubusercontent.com/u/29243953?v=4" width="50;" alt="naiba"/></a>
|
||||
<a href="https://github.com/uubulb" title="UUBulb"><img src="https://avatars.githubusercontent.com/u/35923940?v=4" width="50;" alt="UUBulb"/></a>
|
||||
<a href="https://github.com/AkkiaS7" title="Akkia"><img src="https://avatars.githubusercontent.com/u/68485070?v=4" width="50;" alt="Akkia"/></a>
|
||||
<a href="https://github.com/Erope" title="卖女孩的小火柴"><img src="https://avatars.githubusercontent.com/u/44471469?v=4" width="50;" alt="卖女孩的小火柴"/></a>
|
||||
<a href="https://github.com/uubulb" title="UUBulb"><img src="https://avatars.githubusercontent.com/u/35923940?v=4" width="50;" alt="UUBulb"/></a>
|
||||
<a href="https://github.com/nap0o" title="nap0o"><img src="https://avatars.githubusercontent.com/u/144927971?v=4" width="50;" alt="nap0o"/></a>
|
||||
<a href="https://github.com/dysf888" title="黑歌"><img src="https://avatars.githubusercontent.com/u/47450409?v=4" width="50;" alt="黑歌"/></a>
|
||||
<a href="https://github.com/xykt" title="xykt"><img src="https://avatars.githubusercontent.com/u/152045469?v=4" width="50;" alt="xykt"/></a>
|
||||
@ -87,7 +86,6 @@ You can change the dashboard language in the settings page (`/setting`) after th
|
||||
<a href="https://github.com/Moraxyc" title="Moraxyc"><img src="https://avatars.githubusercontent.com/u/69713071?v=4" width="50;" alt="Moraxyc"/></a>
|
||||
<a href="https://github.com/silver-ymz" title="Mingzhuo Yin"><img src="https://avatars.githubusercontent.com/u/78400701?v=4" width="50;" alt="Mingzhuo Yin"/></a>
|
||||
<a href="https://github.com/MartijnLindeman" title="Martijn Lindeman"><img src="https://avatars.githubusercontent.com/u/78365708?v=4" width="50;" alt="Martijn Lindeman"/></a>
|
||||
<a href="https://github.com/xrgzs" title="MadDogOwner"><img src="https://avatars.githubusercontent.com/u/26499123?v=4" width="50;" alt="MadDogOwner"/></a>
|
||||
<a href="https://github.com/funnyzak" title="Leon"><img src="https://avatars.githubusercontent.com/u/2562087?v=4" width="50;" alt="Leon"/></a>
|
||||
<a href="https://github.com/KorenKrita" title="KorenKrita"><img src="https://avatars.githubusercontent.com/u/22239339?v=4" width="50;" alt="KorenKrita"/></a>
|
||||
<a href="https://github.com/techotaku" title="Ian Li"><img src="https://avatars.githubusercontent.com/u/1948179?v=4" width="50;" alt="Ian Li"/></a>
|
||||
@ -101,4 +99,4 @@ You can change the dashboard language in the settings page (`/setting`) after th
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#naiba/nezha&Timeline)
|
||||
[](https://star-history.com/#nezhahq/nezha&Timeline)
|
||||
|
0
cmd/dashboard/admin-dist/.gitkeep
Normal file
0
cmd/dashboard/admin-dist/.gitkeep
Normal file
173
cmd/dashboard/controller/alertrule.go
Normal file
173
cmd/dashboard/controller/alertrule.go
Normal file
@ -0,0 +1,173 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jinzhu/copier"
|
||||
|
||||
"github.com/nezhahq/nezha/model"
|
||||
"github.com/nezhahq/nezha/service/singleton"
|
||||
)
|
||||
|
||||
// List Alert rules
|
||||
// @Summary List Alert rules
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description List Alert rules
|
||||
// @Tags auth required
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[[]model.AlertRule]
|
||||
// @Router /alert-rule [get]
|
||||
func listAlertRule(c *gin.Context) ([]*model.AlertRule, error) {
|
||||
singleton.AlertsLock.RLock()
|
||||
defer singleton.AlertsLock.RUnlock()
|
||||
|
||||
var ar []*model.AlertRule
|
||||
if err := copier.Copy(&ar, &singleton.Alerts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ar, nil
|
||||
}
|
||||
|
||||
// Add Alert Rule
|
||||
// @Summary Add Alert Rule
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description Add Alert Rule
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @param request body model.AlertRuleForm true "AlertRuleForm"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[uint64]
|
||||
// @Router /alert-rule [post]
|
||||
func createAlertRule(c *gin.Context) (uint64, error) {
|
||||
var arf model.AlertRuleForm
|
||||
var r model.AlertRule
|
||||
|
||||
if err := c.ShouldBindJSON(&arf); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
r.Name = arf.Name
|
||||
r.Rules = arf.Rules
|
||||
r.FailTriggerTasks = arf.FailTriggerTasks
|
||||
r.RecoverTriggerTasks = arf.RecoverTriggerTasks
|
||||
r.NotificationGroupID = arf.NotificationGroupID
|
||||
enable := arf.Enable
|
||||
r.TriggerMode = arf.TriggerMode
|
||||
r.Enable = &enable
|
||||
|
||||
if err := validateRule(&r); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err := singleton.DB.Create(&r).Error; err != nil {
|
||||
return 0, newGormError("%v", err)
|
||||
}
|
||||
|
||||
singleton.OnRefreshOrAddAlert(&r)
|
||||
return r.ID, nil
|
||||
}
|
||||
|
||||
// Update Alert Rule
|
||||
// @Summary Update Alert Rule
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description Update Alert Rule
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @param id path uint true "Alert ID"
|
||||
// @param request body model.AlertRuleForm true "AlertRuleForm"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[any]
|
||||
// @Router /alert-rule/{id} [patch]
|
||||
func updateAlertRule(c *gin.Context) (any, error) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var arf model.AlertRuleForm
|
||||
if err := c.ShouldBindJSON(&arf); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var r model.AlertRule
|
||||
if err := singleton.DB.First(&r, id).Error; err != nil {
|
||||
return nil, singleton.Localizer.ErrorT("alert id %d does not exist", id)
|
||||
}
|
||||
|
||||
r.Name = arf.Name
|
||||
r.Rules = arf.Rules
|
||||
r.FailTriggerTasks = arf.FailTriggerTasks
|
||||
r.RecoverTriggerTasks = arf.RecoverTriggerTasks
|
||||
r.NotificationGroupID = arf.NotificationGroupID
|
||||
enable := arf.Enable
|
||||
r.TriggerMode = arf.TriggerMode
|
||||
r.Enable = &enable
|
||||
|
||||
if err := validateRule(&r); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err := singleton.DB.Save(&r).Error; err != nil {
|
||||
return 0, newGormError("%v", err)
|
||||
}
|
||||
|
||||
singleton.OnRefreshOrAddAlert(&r)
|
||||
return r.ID, nil
|
||||
}
|
||||
|
||||
// Batch delete Alert rules
|
||||
// @Summary Batch delete Alert rules
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description Batch delete Alert rules
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @param request body []uint64 true "id list"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[any]
|
||||
// @Router /batch-delete/alert-rule [post]
|
||||
func batchDeleteAlertRule(c *gin.Context) (any, error) {
|
||||
var ar []uint64
|
||||
|
||||
if err := c.ShouldBindJSON(&ar); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := singleton.DB.Unscoped().Delete(&model.AlertRule{}, "id in (?)", ar).Error; err != nil {
|
||||
return nil, newGormError("%v", err)
|
||||
}
|
||||
|
||||
singleton.OnDeleteAlert(ar)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func validateRule(r *model.AlertRule) error {
|
||||
if len(r.Rules) > 0 {
|
||||
for _, rule := range r.Rules {
|
||||
if !rule.IsTransferDurationRule() {
|
||||
if rule.Duration < 3 {
|
||||
return singleton.Localizer.ErrorT("duration need to be at least 3")
|
||||
}
|
||||
} else {
|
||||
if rule.CycleInterval < 1 {
|
||||
return singleton.Localizer.ErrorT("cycle_interval need to be at least 1")
|
||||
}
|
||||
if rule.CycleStart == nil {
|
||||
return singleton.Localizer.ErrorT("cycle_start is not set")
|
||||
}
|
||||
if rule.CycleStart.After(time.Now()) {
|
||||
return singleton.Localizer.ErrorT("cycle_start is a future value")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return singleton.Localizer.ErrorT("need to configure at least a single rule")
|
||||
}
|
||||
return nil
|
||||
}
|
@ -1,112 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/naiba/nezha/model"
|
||||
"github.com/naiba/nezha/pkg/mygin"
|
||||
"github.com/naiba/nezha/service/singleton"
|
||||
)
|
||||
|
||||
type apiV1 struct {
|
||||
r gin.IRouter
|
||||
}
|
||||
|
||||
func (v *apiV1) serve() {
|
||||
r := v.r.Group("")
|
||||
// 强制认证的 API
|
||||
r.Use(mygin.Authorize(mygin.AuthorizeOption{
|
||||
MemberOnly: true,
|
||||
AllowAPI: true,
|
||||
IsPage: false,
|
||||
Msg: "访问此接口需要认证",
|
||||
Btn: "点此登录",
|
||||
Redirect: "/login",
|
||||
}))
|
||||
r.GET("/server/list", v.serverList)
|
||||
r.GET("/server/details", v.serverDetails)
|
||||
// 不强制认证的 API
|
||||
mr := v.r.Group("monitor")
|
||||
mr.Use(mygin.Authorize(mygin.AuthorizeOption{
|
||||
MemberOnly: false,
|
||||
IsPage: false,
|
||||
AllowAPI: true,
|
||||
Msg: "访问此接口需要认证",
|
||||
Btn: "点此登录",
|
||||
Redirect: "/login",
|
||||
}))
|
||||
mr.Use(mygin.ValidateViewPassword(mygin.ValidateViewPasswordOption{
|
||||
IsPage: false,
|
||||
AbortWhenFail: true,
|
||||
}))
|
||||
mr.GET("/:id", v.monitorHistoriesById)
|
||||
}
|
||||
|
||||
// serverList 获取服务器列表 不传入Query参数则获取全部
|
||||
// header: Authorization: Token
|
||||
// query: tag (服务器分组)
|
||||
func (v *apiV1) serverList(c *gin.Context) {
|
||||
tag := c.Query("tag")
|
||||
if tag != "" {
|
||||
c.JSON(200, singleton.ServerAPI.GetListByTag(tag))
|
||||
return
|
||||
}
|
||||
c.JSON(200, singleton.ServerAPI.GetAllList())
|
||||
}
|
||||
|
||||
// serverDetails 获取服务器信息 不传入Query参数则获取全部
|
||||
// header: Authorization: Token
|
||||
// query: id (服务器ID,逗号分隔,优先级高于tag查询)
|
||||
// query: tag (服务器分组)
|
||||
func (v *apiV1) serverDetails(c *gin.Context) {
|
||||
var idList []uint64
|
||||
idListStr := strings.Split(c.Query("id"), ",")
|
||||
if c.Query("id") != "" {
|
||||
idList = make([]uint64, len(idListStr))
|
||||
for i, v := range idListStr {
|
||||
id, _ := strconv.ParseUint(v, 10, 64)
|
||||
idList[i] = id
|
||||
}
|
||||
}
|
||||
tag := c.Query("tag")
|
||||
if tag != "" {
|
||||
c.JSON(200, singleton.ServerAPI.GetStatusByTag(tag))
|
||||
return
|
||||
}
|
||||
if len(idList) != 0 {
|
||||
c.JSON(200, singleton.ServerAPI.GetStatusByIDList(idList))
|
||||
return
|
||||
}
|
||||
c.JSON(200, singleton.ServerAPI.GetAllStatus())
|
||||
}
|
||||
|
||||
func (v *apiV1) monitorHistoriesById(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(400, gin.H{"code": 400, "message": "id参数错误"})
|
||||
return
|
||||
}
|
||||
server, ok := singleton.ServerList[id]
|
||||
if !ok {
|
||||
c.AbortWithStatusJSON(404, gin.H{
|
||||
"code": 404,
|
||||
"message": "id不存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
_, isMember := c.Get(model.CtxKeyAuthorizedUser)
|
||||
_, isViewPasswordVerfied := c.Get(model.CtxKeyViewPasswordVerified)
|
||||
authorized := isMember || isViewPasswordVerfied
|
||||
|
||||
if server.HideForGuest && !authorized {
|
||||
c.AbortWithStatusJSON(403, gin.H{"code": 403, "message": "需要认证"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, singleton.MonitorAPI.GetMonitorHistories(map[string]any{"server_id": server.ID}))
|
||||
}
|
@ -1,560 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/hashicorp/go-uuid"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/sync/singleflight"
|
||||
|
||||
"github.com/naiba/nezha/model"
|
||||
"github.com/naiba/nezha/pkg/mygin"
|
||||
"github.com/naiba/nezha/pkg/utils"
|
||||
"github.com/naiba/nezha/pkg/websocketx"
|
||||
"github.com/naiba/nezha/proto"
|
||||
"github.com/naiba/nezha/service/rpc"
|
||||
"github.com/naiba/nezha/service/singleton"
|
||||
)
|
||||
|
||||
type commonPage struct {
|
||||
r *gin.Engine
|
||||
requestGroup singleflight.Group
|
||||
}
|
||||
|
||||
func (cp *commonPage) serve() {
|
||||
cr := cp.r.Group("")
|
||||
cr.Use(mygin.Authorize(mygin.AuthorizeOption{}))
|
||||
cr.Use(mygin.PreferredTheme)
|
||||
cr.POST("/view-password", cp.issueViewPassword)
|
||||
cr.GET("/terminal/:id", cp.terminal)
|
||||
cr.Use(mygin.ValidateViewPassword(mygin.ValidateViewPasswordOption{
|
||||
IsPage: true,
|
||||
AbortWhenFail: true,
|
||||
}))
|
||||
cr.GET("/", cp.home)
|
||||
cr.GET("/service", cp.service)
|
||||
// TODO: 界面直接跳转使用该接口
|
||||
cr.GET("/network/:id", cp.network)
|
||||
cr.GET("/network", cp.network)
|
||||
cr.GET("/ws", cp.ws)
|
||||
cr.POST("/terminal", cp.createTerminal)
|
||||
cr.GET("/file", cp.createFM)
|
||||
cr.GET("/file/:id", cp.fm)
|
||||
}
|
||||
|
||||
type viewPasswordForm struct {
|
||||
Password string
|
||||
}
|
||||
|
||||
func (p *commonPage) issueViewPassword(c *gin.Context) {
|
||||
var vpf viewPasswordForm
|
||||
err := c.ShouldBind(&vpf)
|
||||
var hash []byte
|
||||
if err == nil && vpf.Password != singleton.Conf.Site.ViewPassword {
|
||||
err = errors.New(singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "WrongAccessPassword"}))
|
||||
}
|
||||
if err == nil {
|
||||
hash, err = bcrypt.GenerateFromPassword([]byte(vpf.Password), bcrypt.DefaultCost)
|
||||
}
|
||||
if err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusOK,
|
||||
Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
|
||||
MessageID: "AnErrorEccurred",
|
||||
}),
|
||||
Msg: err.Error(),
|
||||
}, true)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.SetCookie(singleton.Conf.Site.CookieName+"-vp", string(hash), 60*60*24, "", "", false, false)
|
||||
c.Redirect(http.StatusFound, c.Request.Referer())
|
||||
}
|
||||
|
||||
func (p *commonPage) service(c *gin.Context) {
|
||||
res, _, _ := p.requestGroup.Do("servicePage", func() (interface{}, error) {
|
||||
singleton.AlertsLock.RLock()
|
||||
defer singleton.AlertsLock.RUnlock()
|
||||
var stats map[uint64]model.ServiceItemResponse
|
||||
var statsStore map[uint64]model.CycleTransferStats
|
||||
copier.Copy(&stats, singleton.ServiceSentinelShared.LoadStats())
|
||||
copier.Copy(&statsStore, singleton.AlertsCycleTransferStatsStore)
|
||||
for k, service := range stats {
|
||||
if !service.Monitor.EnableShowInService {
|
||||
delete(stats, k)
|
||||
}
|
||||
}
|
||||
return []interface {
|
||||
}{
|
||||
stats, statsStore,
|
||||
}, nil
|
||||
})
|
||||
c.HTML(http.StatusOK, mygin.GetPreferredTheme(c, "/service"), mygin.CommonEnvironment(c, gin.H{
|
||||
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ServicesStatus"}),
|
||||
"Services": res.([]interface{})[0],
|
||||
"CycleTransferStats": res.([]interface{})[1],
|
||||
}))
|
||||
}
|
||||
|
||||
func (cp *commonPage) network(c *gin.Context) {
|
||||
var (
|
||||
monitorHistory *model.MonitorHistory
|
||||
servers []*model.Server
|
||||
serverIdsWithMonitor []uint64
|
||||
monitorInfos = []byte("{}")
|
||||
id uint64
|
||||
)
|
||||
if len(singleton.SortedServerList) > 0 {
|
||||
id = singleton.SortedServerList[0].ID
|
||||
}
|
||||
if err := singleton.DB.Model(&model.MonitorHistory{}).Select("monitor_id, server_id").
|
||||
Where("monitor_id != 0 and server_id != 0").Limit(1).First(&monitorHistory).Error; err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusForbidden,
|
||||
Title: "请求失败",
|
||||
Msg: "请求参数有误:" + "server monitor history not found",
|
||||
Link: "/",
|
||||
Btn: "返回重试",
|
||||
}, true)
|
||||
return
|
||||
} else {
|
||||
if monitorHistory == nil || monitorHistory.ServerID == 0 {
|
||||
if len(singleton.SortedServerList) > 0 {
|
||||
id = singleton.SortedServerList[0].ID
|
||||
}
|
||||
} else {
|
||||
id = monitorHistory.ServerID
|
||||
}
|
||||
}
|
||||
|
||||
idStr := c.Param("id")
|
||||
if idStr != "" {
|
||||
var err error
|
||||
id, err = strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusForbidden,
|
||||
Title: "请求失败",
|
||||
Msg: "请求参数有误:" + err.Error(),
|
||||
Link: "/",
|
||||
Btn: "返回重试",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
_, ok := singleton.ServerList[id]
|
||||
if !ok {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusForbidden,
|
||||
Title: "请求失败",
|
||||
Msg: "请求参数有误:" + "server id not found",
|
||||
Link: "/",
|
||||
Btn: "返回重试",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
}
|
||||
monitorHistories := singleton.MonitorAPI.GetMonitorHistories(map[string]any{"server_id": id})
|
||||
monitorInfos, _ = utils.Json.Marshal(monitorHistories)
|
||||
_, isMember := c.Get(model.CtxKeyAuthorizedUser)
|
||||
_, isViewPasswordVerfied := c.Get(model.CtxKeyViewPasswordVerified)
|
||||
|
||||
if err := singleton.DB.Model(&model.MonitorHistory{}).
|
||||
Select("distinct(server_id)").
|
||||
Where("server_id != 0").
|
||||
Find(&serverIdsWithMonitor).
|
||||
Error; err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusForbidden,
|
||||
Title: "请求失败",
|
||||
Msg: "请求参数有误:" + "no server with monitor histories",
|
||||
Link: "/",
|
||||
Btn: "返回重试",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
if isMember || isViewPasswordVerfied {
|
||||
for _, server := range singleton.SortedServerList {
|
||||
for _, id := range serverIdsWithMonitor {
|
||||
if server.ID == id {
|
||||
servers = append(servers, server)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, server := range singleton.SortedServerListForGuest {
|
||||
for _, id := range serverIdsWithMonitor {
|
||||
if server.ID == id {
|
||||
servers = append(servers, server)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
serversBytes, _ := utils.Json.Marshal(Data{
|
||||
Now: time.Now().Unix() * 1000,
|
||||
Servers: servers,
|
||||
})
|
||||
|
||||
c.HTML(http.StatusOK, mygin.GetPreferredTheme(c, "/network"), mygin.CommonEnvironment(c, gin.H{
|
||||
"Servers": string(serversBytes),
|
||||
"MonitorInfos": string(monitorInfos),
|
||||
"MaxTCPPingValue": singleton.Conf.MaxTCPPingValue,
|
||||
}))
|
||||
}
|
||||
|
||||
func (cp *commonPage) getServerStat(c *gin.Context, withPublicNote bool) ([]byte, error) {
|
||||
_, isMember := c.Get(model.CtxKeyAuthorizedUser)
|
||||
_, isViewPasswordVerfied := c.Get(model.CtxKeyViewPasswordVerified)
|
||||
authorized := isMember || isViewPasswordVerfied
|
||||
v, err, _ := cp.requestGroup.Do(fmt.Sprintf("serverStats::%t", authorized), func() (interface{}, error) {
|
||||
singleton.SortedServerLock.RLock()
|
||||
defer singleton.SortedServerLock.RUnlock()
|
||||
|
||||
var serverList []*model.Server
|
||||
if authorized {
|
||||
serverList = singleton.SortedServerList
|
||||
} else {
|
||||
serverList = singleton.SortedServerListForGuest
|
||||
}
|
||||
|
||||
var servers []*model.Server
|
||||
for _, server := range serverList {
|
||||
item := *server
|
||||
if !withPublicNote {
|
||||
item.PublicNote = ""
|
||||
}
|
||||
servers = append(servers, &item)
|
||||
}
|
||||
|
||||
return utils.Json.Marshal(Data{
|
||||
Now: time.Now().Unix() * 1000,
|
||||
Servers: servers,
|
||||
})
|
||||
})
|
||||
return v.([]byte), err
|
||||
}
|
||||
|
||||
func (cp *commonPage) home(c *gin.Context) {
|
||||
stat, err := cp.getServerStat(c, true)
|
||||
if err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusInternalServerError,
|
||||
Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
|
||||
MessageID: "SystemError",
|
||||
}),
|
||||
Msg: "服务器状态获取失败",
|
||||
Link: "/",
|
||||
Btn: "返回首页",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
c.HTML(http.StatusOK, mygin.GetPreferredTheme(c, "/home"), mygin.CommonEnvironment(c, gin.H{
|
||||
"Servers": string(stat),
|
||||
}))
|
||||
}
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 32768,
|
||||
WriteBufferSize: 32768,
|
||||
}
|
||||
|
||||
type Data struct {
|
||||
Now int64 `json:"now,omitempty"`
|
||||
Servers []*model.Server `json:"servers,omitempty"`
|
||||
}
|
||||
|
||||
func (cp *commonPage) ws(c *gin.Context) {
|
||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusInternalServerError,
|
||||
Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
|
||||
MessageID: "NetworkError",
|
||||
}),
|
||||
Msg: "Websocket协议切换失败",
|
||||
Link: "/",
|
||||
Btn: "返回首页",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
count := 0
|
||||
for {
|
||||
stat, err := cp.getServerStat(c, false)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if err := conn.WriteMessage(websocket.TextMessage, stat); err != nil {
|
||||
break
|
||||
}
|
||||
count += 1
|
||||
if count%4 == 0 {
|
||||
err = conn.WriteMessage(websocket.PingMessage, []byte{})
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
time.Sleep(time.Second * 2)
|
||||
}
|
||||
}
|
||||
|
||||
func (cp *commonPage) terminal(c *gin.Context) {
|
||||
streamId := c.Param("id")
|
||||
if _, err := rpc.NezhaHandlerSingleton.GetStream(streamId); err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusForbidden,
|
||||
Title: "无权访问",
|
||||
Msg: "终端会话不存在",
|
||||
Link: "/",
|
||||
Btn: "返回首页",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
defer rpc.NezhaHandlerSingleton.CloseStream(streamId)
|
||||
|
||||
wsConn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusInternalServerError,
|
||||
Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
|
||||
MessageID: "NetworkError",
|
||||
}),
|
||||
Msg: "Websocket协议切换失败",
|
||||
Link: "/",
|
||||
Btn: "返回首页",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
defer wsConn.Close()
|
||||
conn := websocketx.NewConn(wsConn)
|
||||
|
||||
go func() {
|
||||
// PING 保活
|
||||
for {
|
||||
if err = conn.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
|
||||
return
|
||||
}
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}()
|
||||
|
||||
if err = rpc.NezhaHandlerSingleton.UserConnected(streamId, conn); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
rpc.NezhaHandlerSingleton.StartStream(streamId, time.Second*10)
|
||||
}
|
||||
|
||||
type createTerminalRequest struct {
|
||||
Host string
|
||||
Protocol string
|
||||
ID uint64
|
||||
}
|
||||
|
||||
func (cp *commonPage) createTerminal(c *gin.Context) {
|
||||
if _, authorized := c.Get(model.CtxKeyAuthorizedUser); !authorized {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusForbidden,
|
||||
Title: "无权访问",
|
||||
Msg: "用户未登录",
|
||||
Link: "/login",
|
||||
Btn: "去登录",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
var createTerminalReq createTerminalRequest
|
||||
if err := c.ShouldBind(&createTerminalReq); err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusForbidden,
|
||||
Title: "请求失败",
|
||||
Msg: "请求参数有误:" + err.Error(),
|
||||
Link: "/server",
|
||||
Btn: "返回重试",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
|
||||
streamId, err := uuid.GenerateUUID()
|
||||
if err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusInternalServerError,
|
||||
Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
|
||||
MessageID: "SystemError",
|
||||
}),
|
||||
Msg: "生成会话ID失败",
|
||||
Link: "/server",
|
||||
Btn: "返回重试",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
|
||||
rpc.NezhaHandlerSingleton.CreateStream(streamId)
|
||||
|
||||
singleton.ServerLock.RLock()
|
||||
server := singleton.ServerList[createTerminalReq.ID]
|
||||
singleton.ServerLock.RUnlock()
|
||||
if server == nil || server.TaskStream == nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusForbidden,
|
||||
Title: "请求失败",
|
||||
Msg: "服务器不存在或处于离线状态",
|
||||
Link: "/server",
|
||||
Btn: "返回重试",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
|
||||
terminalData, _ := utils.Json.Marshal(&model.TerminalTask{
|
||||
StreamID: streamId,
|
||||
})
|
||||
if err := server.TaskStream.Send(&proto.Task{
|
||||
Type: model.TaskTypeTerminalGRPC,
|
||||
Data: string(terminalData),
|
||||
}); err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusForbidden,
|
||||
Title: "请求失败",
|
||||
Msg: "Agent信令下发失败",
|
||||
Link: "/server",
|
||||
Btn: "返回重试",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/terminal", mygin.CommonEnvironment(c, gin.H{
|
||||
"SessionID": streamId,
|
||||
"ServerName": server.Name,
|
||||
"ServerID": server.ID,
|
||||
}))
|
||||
}
|
||||
|
||||
func (cp *commonPage) fm(c *gin.Context) {
|
||||
streamId := c.Param("id")
|
||||
if _, err := rpc.NezhaHandlerSingleton.GetStream(streamId); err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusForbidden,
|
||||
Title: "无权访问",
|
||||
Msg: "FM会话不存在",
|
||||
Link: "/",
|
||||
Btn: "返回首页",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
defer rpc.NezhaHandlerSingleton.CloseStream(streamId)
|
||||
|
||||
wsConn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusInternalServerError,
|
||||
Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
|
||||
MessageID: "NetworkError",
|
||||
}),
|
||||
Msg: "Websocket协议切换失败",
|
||||
Link: "/",
|
||||
Btn: "返回首页",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
defer wsConn.Close()
|
||||
conn := websocketx.NewConn(wsConn)
|
||||
|
||||
go func() {
|
||||
// PING 保活
|
||||
for {
|
||||
if err = conn.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
|
||||
return
|
||||
}
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}()
|
||||
|
||||
if err = rpc.NezhaHandlerSingleton.UserConnected(streamId, conn); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
rpc.NezhaHandlerSingleton.StartStream(streamId, time.Second*10)
|
||||
}
|
||||
|
||||
func (cp *commonPage) createFM(c *gin.Context) {
|
||||
IdString := c.Query("id")
|
||||
if _, authorized := c.Get(model.CtxKeyAuthorizedUser); !authorized {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusForbidden,
|
||||
Title: "无权访问",
|
||||
Msg: "用户未登录",
|
||||
Link: "/login",
|
||||
Btn: "去登录",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
|
||||
streamId, err := uuid.GenerateUUID()
|
||||
if err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusInternalServerError,
|
||||
Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
|
||||
MessageID: "SystemError",
|
||||
}),
|
||||
Msg: "生成会话ID失败",
|
||||
Link: "/server",
|
||||
Btn: "返回重试",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
|
||||
rpc.NezhaHandlerSingleton.CreateStream(streamId)
|
||||
|
||||
serverId, err := strconv.Atoi(IdString)
|
||||
if err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusForbidden,
|
||||
Title: "请求失败",
|
||||
Msg: "请求参数有误:" + err.Error(),
|
||||
Link: "/server",
|
||||
Btn: "返回重试",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
|
||||
singleton.ServerLock.RLock()
|
||||
server := singleton.ServerList[uint64(serverId)]
|
||||
singleton.ServerLock.RUnlock()
|
||||
if server == nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusForbidden,
|
||||
Title: "请求失败",
|
||||
Msg: "服务器不存在或处于离线状态",
|
||||
Link: "/server",
|
||||
Btn: "返回重试",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
|
||||
fmData, _ := utils.Json.Marshal(&model.TaskFM{
|
||||
StreamID: streamId,
|
||||
})
|
||||
if err := server.TaskStream.Send(&proto.Task{
|
||||
Type: model.TaskTypeFM,
|
||||
Data: string(fmData),
|
||||
}); err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusForbidden,
|
||||
Title: "请求失败",
|
||||
Msg: "Agent信令下发失败",
|
||||
Link: "/server",
|
||||
Btn: "返回重试",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/file", mygin.CommonEnvironment(c, gin.H{
|
||||
"SessionID": streamId,
|
||||
}))
|
||||
}
|
@ -1,345 +1,259 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.cloudfoundry.org/bytefmt"
|
||||
jwt "github.com/appleboy/gin-jwt/v2"
|
||||
"github.com/gin-contrib/pprof"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/hashicorp/go-uuid"
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
swaggerfiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
|
||||
"github.com/naiba/nezha/model"
|
||||
"github.com/naiba/nezha/pkg/mygin"
|
||||
"github.com/naiba/nezha/pkg/utils"
|
||||
"github.com/naiba/nezha/proto"
|
||||
"github.com/naiba/nezha/resource"
|
||||
"github.com/naiba/nezha/service/rpc"
|
||||
"github.com/naiba/nezha/service/singleton"
|
||||
"github.com/nezhahq/nezha/cmd/dashboard/controller/waf"
|
||||
docs "github.com/nezhahq/nezha/cmd/dashboard/docs"
|
||||
"github.com/nezhahq/nezha/model"
|
||||
"github.com/nezhahq/nezha/service/singleton"
|
||||
)
|
||||
|
||||
func ServeWeb(port uint) *http.Server {
|
||||
func ServeWeb(adminFrontend, userFrontend fs.FS) http.Handler {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
r := gin.Default()
|
||||
|
||||
if singleton.Conf.Debug {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
pprof.Register(r)
|
||||
}
|
||||
r.Use(natGateway)
|
||||
tmpl := template.New("").Funcs(funcMap)
|
||||
var err error
|
||||
tmpl, err = tmpl.ParseFS(resource.TemplateFS, "template/**/*.html")
|
||||
if singleton.Conf.Debug {
|
||||
log.Printf("NEZHA>> Swagger(%s) UI available at http://localhost:%d/swagger/index.html", docs.SwaggerInfo.Version, singleton.Conf.ListenPort)
|
||||
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
|
||||
}
|
||||
|
||||
r.Use(waf.RealIp)
|
||||
r.Use(waf.Waf)
|
||||
r.Use(recordPath)
|
||||
|
||||
routers(r, adminFrontend, userFrontend)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func routers(r *gin.Engine, adminFrontend, userFrontend fs.FS) {
|
||||
authMiddleware, err := jwt.New(initParams())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.Fatal("JWT Error:" + err.Error())
|
||||
}
|
||||
tmpl = loadThirdPartyTemplates(tmpl)
|
||||
r.SetHTMLTemplate(tmpl)
|
||||
r.Use(mygin.RecordPath)
|
||||
r.StaticFS("/static", http.FS(resource.StaticFS))
|
||||
routers(r)
|
||||
page404 := func(c *gin.Context) {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusNotFound,
|
||||
Title: "该页面不存在",
|
||||
Msg: "该页面内容可能已着陆火星",
|
||||
Link: "/",
|
||||
Btn: "返回首页",
|
||||
}, true)
|
||||
if err := authMiddleware.MiddlewareInit(); err != nil {
|
||||
log.Fatal("authMiddleware.MiddlewareInit Error:" + err.Error())
|
||||
}
|
||||
r.NoRoute(page404)
|
||||
r.NoMethod(page404)
|
||||
api := r.Group("api/v1")
|
||||
api.POST("/login", authMiddleware.LoginHandler)
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", port),
|
||||
ReadHeaderTimeout: time.Second * 5,
|
||||
Handler: r,
|
||||
}
|
||||
return srv
|
||||
optionalAuth := api.Group("", optionalAuthMiddleware(authMiddleware))
|
||||
optionalAuth.GET("/ws/server", commonHandler(serverStream))
|
||||
optionalAuth.GET("/server-group", commonHandler(listServerGroup))
|
||||
|
||||
optionalAuth.GET("/service", commonHandler(listService))
|
||||
optionalAuth.GET("/service/:id", commonHandler(listServiceHistory))
|
||||
optionalAuth.GET("/service/server", commonHandler(listServerWithServices))
|
||||
|
||||
optionalAuth.GET("/setting", commonHandler(listConfig))
|
||||
|
||||
auth := api.Group("", authMiddleware.MiddlewareFunc())
|
||||
|
||||
auth.GET("/refresh-token", authMiddleware.RefreshHandler)
|
||||
|
||||
auth.POST("/terminal", commonHandler(createTerminal))
|
||||
auth.GET("/ws/terminal/:id", commonHandler(terminalStream))
|
||||
|
||||
auth.GET("/file", commonHandler(createFM))
|
||||
auth.GET("/ws/file/:id", commonHandler(fmStream))
|
||||
|
||||
auth.GET("/profile", commonHandler(getProfile))
|
||||
auth.POST("/profile", commonHandler(updateProfile))
|
||||
auth.GET("/user", commonHandler(listUser))
|
||||
auth.POST("/user", commonHandler(createUser))
|
||||
auth.POST("/batch-delete/user", commonHandler(batchDeleteUser))
|
||||
|
||||
auth.POST("/service", commonHandler(createService))
|
||||
auth.PATCH("/service/:id", commonHandler(updateService))
|
||||
auth.POST("/batch-delete/service", commonHandler(batchDeleteService))
|
||||
|
||||
auth.POST("/server-group", commonHandler(createServerGroup))
|
||||
auth.PATCH("/server-group/:id", commonHandler(updateServerGroup))
|
||||
auth.POST("/batch-delete/server-group", commonHandler(batchDeleteServerGroup))
|
||||
|
||||
auth.GET("/notification-group", commonHandler(listNotificationGroup))
|
||||
auth.POST("/notification-group", commonHandler(createNotificationGroup))
|
||||
auth.PATCH("/notification-group/:id", commonHandler(updateNotificationGroup))
|
||||
auth.POST("/batch-delete/notification-group", commonHandler(batchDeleteNotificationGroup))
|
||||
|
||||
auth.GET("/server", commonHandler(listServer))
|
||||
auth.PATCH("/server/:id", commonHandler(updateServer))
|
||||
auth.POST("/batch-delete/server", commonHandler(batchDeleteServer))
|
||||
auth.POST("/force-update/server", commonHandler(forceUpdateServer))
|
||||
|
||||
auth.GET("/notification", commonHandler(listNotification))
|
||||
auth.POST("/notification", commonHandler(createNotification))
|
||||
auth.PATCH("/notification/:id", commonHandler(updateNotification))
|
||||
auth.POST("/batch-delete/notification", commonHandler(batchDeleteNotification))
|
||||
|
||||
auth.GET("/alert-rule", commonHandler(listAlertRule))
|
||||
auth.POST("/alert-rule", commonHandler(createAlertRule))
|
||||
auth.PATCH("/alert-rule/:id", commonHandler(updateAlertRule))
|
||||
auth.POST("/batch-delete/alert-rule", commonHandler(batchDeleteAlertRule))
|
||||
|
||||
auth.GET("/cron", commonHandler(listCron))
|
||||
auth.POST("/cron", commonHandler(createCron))
|
||||
auth.PATCH("/cron/:id", commonHandler(updateCron))
|
||||
auth.GET("/cron/:id/manual", commonHandler(manualTriggerCron))
|
||||
auth.POST("/batch-delete/cron", commonHandler(batchDeleteCron))
|
||||
|
||||
auth.GET("/ddns", commonHandler(listDDNS))
|
||||
auth.GET("/ddns/providers", commonHandler(listProviders))
|
||||
auth.POST("/ddns", commonHandler(createDDNS))
|
||||
auth.PATCH("/ddns/:id", commonHandler(updateDDNS))
|
||||
auth.POST("/batch-delete/ddns", commonHandler(batchDeleteDDNS))
|
||||
|
||||
auth.GET("/nat", commonHandler(listNAT))
|
||||
auth.POST("/nat", commonHandler(createNAT))
|
||||
auth.PATCH("/nat/:id", commonHandler(updateNAT))
|
||||
auth.POST("/batch-delete/nat", commonHandler(batchDeleteNAT))
|
||||
|
||||
auth.GET("/waf", commonHandler(listBlockedAddress))
|
||||
auth.POST("/batch-delete/waf", commonHandler(batchDeleteBlockedAddress))
|
||||
|
||||
auth.PATCH("/setting", commonHandler(updateConfig))
|
||||
|
||||
r.NoRoute(fallbackToFrontend(adminFrontend, userFrontend))
|
||||
}
|
||||
|
||||
func routers(r *gin.Engine) {
|
||||
// 通用页面
|
||||
cp := commonPage{r: r}
|
||||
cp.serve()
|
||||
// 游客页面
|
||||
gp := guestPage{r}
|
||||
gp.serve()
|
||||
// 会员页面
|
||||
mp := &memberPage{r}
|
||||
mp.serve()
|
||||
// API
|
||||
api := r.Group("api")
|
||||
{
|
||||
ma := &memberAPI{api}
|
||||
ma.serve()
|
||||
func recordPath(c *gin.Context) {
|
||||
url := c.Request.URL.String()
|
||||
for _, p := range c.Params {
|
||||
url = strings.Replace(url, p.Value, ":"+p.Key, 1)
|
||||
}
|
||||
c.Set("MatchedPath", url)
|
||||
}
|
||||
|
||||
func newErrorResponse(err error) model.CommonResponse[any] {
|
||||
return model.CommonResponse[any]{
|
||||
Success: false,
|
||||
Error: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
func loadThirdPartyTemplates(tmpl *template.Template) *template.Template {
|
||||
ret := tmpl
|
||||
themes, err := os.ReadDir("resource/template")
|
||||
if err != nil {
|
||||
log.Printf("NEZHA>> Error reading themes folder: %v", err)
|
||||
return ret
|
||||
}
|
||||
for _, theme := range themes {
|
||||
if !theme.IsDir() {
|
||||
continue
|
||||
}
|
||||
type handlerFunc[T any] func(c *gin.Context) (T, error)
|
||||
|
||||
themeDir := theme.Name()
|
||||
if themeDir == "theme-custom" {
|
||||
// for backward compatibility
|
||||
// note: will remove this in future versions
|
||||
ret = loadTemplates(ret, themeDir)
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(themeDir, "dashboard-") {
|
||||
// load dashboard templates, ignore desc file
|
||||
ret = loadTemplates(ret, themeDir)
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(themeDir, "theme-") {
|
||||
log.Printf("NEZHA>> Invalid theme name: %s", themeDir)
|
||||
continue
|
||||
}
|
||||
|
||||
descPath := filepath.Join("resource", "template", themeDir, "theme.json")
|
||||
desc, err := os.ReadFile(filepath.Clean(descPath))
|
||||
if err != nil {
|
||||
log.Printf("NEZHA>> Error opening %s config: %v", themeDir, err)
|
||||
continue
|
||||
}
|
||||
|
||||
themeName, err := utils.GjsonGet(desc, "name")
|
||||
if err != nil {
|
||||
log.Printf("NEZHA>> Error opening %s config: not a valid description file", theme.Name())
|
||||
continue
|
||||
}
|
||||
|
||||
// load templates
|
||||
ret = loadTemplates(ret, themeDir)
|
||||
|
||||
themeKey := strings.TrimPrefix(themeDir, "theme-")
|
||||
model.Themes[themeKey] = themeName.String()
|
||||
}
|
||||
|
||||
return ret
|
||||
// There are many error types in gorm, so create a custom type to represent all
|
||||
// gorm errors here instead
|
||||
type gormError struct {
|
||||
msg string
|
||||
a []interface{}
|
||||
}
|
||||
|
||||
func loadTemplates(tmpl *template.Template, themeDir string) *template.Template {
|
||||
// load templates
|
||||
templatePath := filepath.Join("resource", "template", themeDir, "*.html")
|
||||
t, err := tmpl.ParseGlob(templatePath)
|
||||
if err != nil {
|
||||
log.Printf("NEZHA>> Error parsing templates %s: %v", themeDir, err)
|
||||
return tmpl
|
||||
func newGormError(format string, args ...interface{}) error {
|
||||
return &gormError{
|
||||
msg: format,
|
||||
a: args,
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
var funcMap = template.FuncMap{
|
||||
"tr": func(id string, dataAndCount ...interface{}) string {
|
||||
conf := i18n.LocalizeConfig{
|
||||
MessageID: id,
|
||||
func (ge *gormError) Error() string {
|
||||
return fmt.Sprintf(ge.msg, ge.a...)
|
||||
}
|
||||
|
||||
type wsError struct {
|
||||
msg string
|
||||
a []interface{}
|
||||
}
|
||||
|
||||
func newWsError(format string, args ...interface{}) error {
|
||||
return &wsError{
|
||||
msg: format,
|
||||
a: args,
|
||||
}
|
||||
}
|
||||
|
||||
func (we *wsError) Error() string {
|
||||
return fmt.Sprintf(we.msg, we.a...)
|
||||
}
|
||||
|
||||
func commonHandler[T any](handler handlerFunc[T]) func(*gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
data, err := handler(c)
|
||||
if err == nil {
|
||||
c.JSON(http.StatusOK, model.CommonResponse[T]{Success: true, Data: data})
|
||||
return
|
||||
}
|
||||
if len(dataAndCount) > 0 {
|
||||
conf.TemplateData = dataAndCount[0]
|
||||
}
|
||||
if len(dataAndCount) > 1 {
|
||||
conf.PluralCount = dataAndCount[1]
|
||||
}
|
||||
return singleton.Localizer.MustLocalize(&conf)
|
||||
},
|
||||
"toValMap": func(val interface{}) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"Value": val,
|
||||
}
|
||||
},
|
||||
"tf": func(t time.Time) string {
|
||||
return t.In(singleton.Loc).Format("01/02/2006 15:04:05")
|
||||
},
|
||||
"len": func(slice []interface{}) string {
|
||||
return strconv.Itoa(len(slice))
|
||||
},
|
||||
"safe": func(s string) template.HTML {
|
||||
return template.HTML(s) // #nosec
|
||||
},
|
||||
"tag": func(s string) template.HTML {
|
||||
return template.HTML(`<` + s + `>`) // #nosec
|
||||
},
|
||||
"stf": func(s uint64) string {
|
||||
return time.Unix(int64(s), 0).In(singleton.Loc).Format("01/02/2006 15:04")
|
||||
},
|
||||
"sf": func(duration uint64) string {
|
||||
return time.Duration(time.Duration(duration) * time.Second).String()
|
||||
},
|
||||
"sft": func(future time.Time) string {
|
||||
return time.Until(future).Round(time.Second).String()
|
||||
},
|
||||
"bf": func(b uint64) string {
|
||||
return bytefmt.ByteSize(b)
|
||||
},
|
||||
"ts": func(s string) string {
|
||||
return strings.TrimSpace(s)
|
||||
},
|
||||
"float32f": func(f float32) string {
|
||||
return fmt.Sprintf("%.3f", f)
|
||||
},
|
||||
"divU64": func(a, b uint64) float32 {
|
||||
if b == 0 {
|
||||
if a > 0 {
|
||||
return 100
|
||||
switch err.(type) {
|
||||
case *gormError:
|
||||
log.Printf("NEZHA>> gorm error: %v", err)
|
||||
c.JSON(http.StatusOK, newErrorResponse(singleton.Localizer.ErrorT("database error")))
|
||||
return
|
||||
case *wsError:
|
||||
// Connection is upgraded to WebSocket, so c.Writer is no longer usable
|
||||
if msg := err.Error(); msg != "" {
|
||||
log.Printf("NEZHA>> websocket error: %v", err)
|
||||
}
|
||||
return 0
|
||||
return
|
||||
default:
|
||||
c.JSON(http.StatusOK, newErrorResponse(err))
|
||||
return
|
||||
}
|
||||
if a == 0 {
|
||||
// 这是从未在线的情况
|
||||
return 0.00001 / float32(b) * 100
|
||||
}
|
||||
}
|
||||
|
||||
func fallbackToFrontend(adminFrontend, userFrontend fs.FS) func(*gin.Context) {
|
||||
checkLocalFileOrFs := func(c *gin.Context, fs fs.FS, path string) bool {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
c.File(path)
|
||||
return true
|
||||
}
|
||||
return float32(a) / float32(b) * 100
|
||||
},
|
||||
"div": func(a, b int) float32 {
|
||||
if b == 0 {
|
||||
if a > 0 {
|
||||
return 100
|
||||
f, err := fs.Open(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
fileStat, err := f.Stat()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if fileStat.IsDir() {
|
||||
return false
|
||||
}
|
||||
http.ServeContent(c.Writer, c.Request, path, fileStat.ModTime(), f.(io.ReadSeeker))
|
||||
return true
|
||||
}
|
||||
return func(c *gin.Context) {
|
||||
if strings.HasPrefix(c.Request.URL.Path, "/api") {
|
||||
c.JSON(http.StatusOK, newErrorResponse(errors.New("404 Not Found")))
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(c.Request.URL.Path, "/dashboard") {
|
||||
stripPath := strings.TrimPrefix(c.Request.URL.Path, "/dashboard")
|
||||
localFilePath := path.Join("admin-dist", stripPath)
|
||||
if checkLocalFileOrFs(c, adminFrontend, localFilePath) {
|
||||
return
|
||||
}
|
||||
return 0
|
||||
if !checkLocalFileOrFs(c, adminFrontend, "admin-dist/index.html") {
|
||||
c.JSON(http.StatusOK, newErrorResponse(errors.New("404 Not Found")))
|
||||
}
|
||||
return
|
||||
}
|
||||
if a == 0 {
|
||||
// 这是从未在线的情况
|
||||
return 0.00001 / float32(b) * 100
|
||||
localFilePath := path.Join("user-dist", c.Request.URL.Path)
|
||||
if checkLocalFileOrFs(c, userFrontend, localFilePath) {
|
||||
return
|
||||
}
|
||||
return float32(a) / float32(b) * 100
|
||||
},
|
||||
"addU64": func(a, b uint64) uint64 {
|
||||
return a + b
|
||||
},
|
||||
"add": func(a, b int) int {
|
||||
return a + b
|
||||
},
|
||||
"TransLeftPercent": func(a, b float64) (n float64) {
|
||||
n, _ = strconv.ParseFloat(fmt.Sprintf("%.2f", (100-(a/b)*100)), 64)
|
||||
if n < 0 {
|
||||
n = 0
|
||||
if !checkLocalFileOrFs(c, userFrontend, "user-dist/index.html") {
|
||||
c.JSON(http.StatusOK, newErrorResponse(errors.New("404 Not Found")))
|
||||
}
|
||||
return
|
||||
},
|
||||
"TransLeft": func(a, b uint64) string {
|
||||
if a < b {
|
||||
return "0B"
|
||||
}
|
||||
return bytefmt.ByteSize(a - b)
|
||||
},
|
||||
"TransClassName": func(a float64) string {
|
||||
if a == 0 {
|
||||
return "offline"
|
||||
}
|
||||
if a > 50 {
|
||||
return "fine"
|
||||
}
|
||||
if a > 20 {
|
||||
return "warning"
|
||||
}
|
||||
if a > 0 {
|
||||
return "error"
|
||||
}
|
||||
return "offline"
|
||||
},
|
||||
"UintToFloat": func(a uint64) (n float64) {
|
||||
n, _ = strconv.ParseFloat((strconv.FormatUint(a, 10)), 64)
|
||||
return
|
||||
},
|
||||
"dayBefore": func(i int) string {
|
||||
year, month, day := time.Now().Date()
|
||||
today := time.Date(year, month, day, 0, 0, 0, 0, singleton.Loc)
|
||||
return today.AddDate(0, 0, i-29).Format("01/02")
|
||||
},
|
||||
"className": func(percent float32) string {
|
||||
if percent == 0 {
|
||||
return ""
|
||||
}
|
||||
if percent > 95 {
|
||||
return "good"
|
||||
}
|
||||
if percent > 80 {
|
||||
return "warning"
|
||||
}
|
||||
return "danger"
|
||||
},
|
||||
"statusName": func(val float32) string {
|
||||
return singleton.StatusCodeToString(singleton.GetStatusCode(val))
|
||||
},
|
||||
}
|
||||
|
||||
func natGateway(c *gin.Context) {
|
||||
natConfig := singleton.GetNATConfigByDomain(c.Request.Host)
|
||||
if natConfig == nil {
|
||||
return
|
||||
}
|
||||
|
||||
singleton.ServerLock.RLock()
|
||||
server := singleton.ServerList[natConfig.ServerID]
|
||||
singleton.ServerLock.RUnlock()
|
||||
if server == nil || server.TaskStream == nil {
|
||||
c.Writer.WriteString("server not found or not connected")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
streamId, err := uuid.GenerateUUID()
|
||||
if err != nil {
|
||||
c.Writer.WriteString(fmt.Sprintf("stream id error: %v", err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
rpc.NezhaHandlerSingleton.CreateStream(streamId)
|
||||
defer rpc.NezhaHandlerSingleton.CloseStream(streamId)
|
||||
|
||||
taskData, err := utils.Json.Marshal(model.TaskNAT{
|
||||
StreamID: streamId,
|
||||
Host: natConfig.Host,
|
||||
})
|
||||
if err != nil {
|
||||
c.Writer.WriteString(fmt.Sprintf("task data error: %v", err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if err := server.TaskStream.Send(&proto.Task{
|
||||
Type: model.TaskTypeNAT,
|
||||
Data: string(taskData),
|
||||
}); err != nil {
|
||||
c.Writer.WriteString(fmt.Sprintf("send task error: %v", err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
w, err := utils.NewRequestWrapper(c.Request, c.Writer)
|
||||
if err != nil {
|
||||
c.Writer.WriteString(fmt.Sprintf("request wrapper error: %v", err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if err := rpc.NezhaHandlerSingleton.UserConnected(streamId, w); err != nil {
|
||||
c.Writer.WriteString(fmt.Sprintf("user connected error: %v", err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
rpc.NezhaHandlerSingleton.StartStream(streamId, time.Second*10)
|
||||
c.Abort()
|
||||
}
|
||||
}
|
||||
|
193
cmd/dashboard/controller/cron.go
Normal file
193
cmd/dashboard/controller/cron.go
Normal file
@ -0,0 +1,193 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jinzhu/copier"
|
||||
|
||||
"github.com/nezhahq/nezha/model"
|
||||
"github.com/nezhahq/nezha/service/singleton"
|
||||
)
|
||||
|
||||
// List schedule tasks
|
||||
// @Summary List schedule tasks
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description List schedule tasks
|
||||
// @Tags auth required
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[[]model.Cron]
|
||||
// @Router /cron [get]
|
||||
func listCron(c *gin.Context) ([]*model.Cron, error) {
|
||||
singleton.CronLock.RLock()
|
||||
defer singleton.CronLock.RUnlock()
|
||||
|
||||
var cr []*model.Cron
|
||||
if err := copier.Copy(&cr, &singleton.CronList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cr, nil
|
||||
}
|
||||
|
||||
// Create new schedule task
|
||||
// @Summary Create new schedule task
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description Create new schedule task
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @param request body model.CronForm true "CronForm"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[uint64]
|
||||
// @Router /cron [post]
|
||||
func createCron(c *gin.Context) (uint64, error) {
|
||||
var cf model.CronForm
|
||||
var cr model.Cron
|
||||
|
||||
if err := c.ShouldBindJSON(&cf); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
cr.TaskType = cf.TaskType
|
||||
cr.Name = cf.Name
|
||||
cr.Scheduler = cf.Scheduler
|
||||
cr.Command = cf.Command
|
||||
cr.Servers = cf.Servers
|
||||
cr.PushSuccessful = cf.PushSuccessful
|
||||
cr.NotificationGroupID = cf.NotificationGroupID
|
||||
cr.Cover = cf.Cover
|
||||
|
||||
if cr.TaskType == model.CronTypeCronTask && cr.Cover == model.CronCoverAlertTrigger {
|
||||
return 0, singleton.Localizer.ErrorT("scheduled tasks cannot be triggered by alarms")
|
||||
}
|
||||
|
||||
// 对于计划任务类型,需要更新CronJob
|
||||
var err error
|
||||
if cf.TaskType == model.CronTypeCronTask {
|
||||
if cr.CronJobID, err = singleton.Cron.AddFunc(cr.Scheduler, singleton.CronTrigger(&cr)); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
if err = singleton.DB.Create(&cr).Error; err != nil {
|
||||
return 0, newGormError("%v", err)
|
||||
}
|
||||
|
||||
singleton.OnRefreshOrAddCron(&cr)
|
||||
singleton.UpdateCronList()
|
||||
return cr.ID, nil
|
||||
}
|
||||
|
||||
// Update schedule task
|
||||
// @Summary Update schedule task
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description Update schedule task
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @param id path uint true "Task ID"
|
||||
// @param request body model.CronForm true "CronForm"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[any]
|
||||
// @Router /cron/{id} [patch]
|
||||
func updateCron(c *gin.Context) (any, error) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var cf model.CronForm
|
||||
if err := c.ShouldBindJSON(&cf); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var cr model.Cron
|
||||
if err := singleton.DB.First(&cr, id).Error; err != nil {
|
||||
return nil, fmt.Errorf("task id %d does not exist", id)
|
||||
}
|
||||
|
||||
cr.TaskType = cf.TaskType
|
||||
cr.Name = cf.Name
|
||||
cr.Scheduler = cf.Scheduler
|
||||
cr.Command = cf.Command
|
||||
cr.Servers = cf.Servers
|
||||
cr.PushSuccessful = cf.PushSuccessful
|
||||
cr.NotificationGroupID = cf.NotificationGroupID
|
||||
cr.Cover = cf.Cover
|
||||
|
||||
if cr.TaskType == model.CronTypeCronTask && cr.Cover == model.CronCoverAlertTrigger {
|
||||
return nil, singleton.Localizer.ErrorT("scheduled tasks cannot be triggered by alarms")
|
||||
}
|
||||
|
||||
// 对于计划任务类型,需要更新CronJob
|
||||
if cf.TaskType == model.CronTypeCronTask {
|
||||
if cr.CronJobID, err = singleton.Cron.AddFunc(cr.Scheduler, singleton.CronTrigger(&cr)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err = singleton.DB.Save(&cr).Error; err != nil {
|
||||
return nil, newGormError("%v", err)
|
||||
}
|
||||
|
||||
singleton.OnRefreshOrAddCron(&cr)
|
||||
singleton.UpdateCronList()
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Trigger schedule task
|
||||
// @Summary Trigger schedule task
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description Trigger schedule task
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @param id path uint true "Task ID"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[any]
|
||||
// @Router /cron/{id}/manual [get]
|
||||
func manualTriggerCron(c *gin.Context) (any, error) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var cr model.Cron
|
||||
if err := singleton.DB.First(&cr, id).Error; err != nil {
|
||||
return nil, singleton.Localizer.ErrorT("task id %d does not exist", id)
|
||||
}
|
||||
|
||||
singleton.ManualTrigger(&cr)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Batch delete schedule tasks
|
||||
// @Summary Batch delete schedule tasks
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description Batch delete schedule tasks
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @param request body []uint64 true "id list"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[any]
|
||||
// @Router /batch-delete/cron [post]
|
||||
func batchDeleteCron(c *gin.Context) (any, error) {
|
||||
var cr []uint64
|
||||
|
||||
if err := c.ShouldBindJSON(&cr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := singleton.DB.Unscoped().Delete(&model.Cron{}, "id in (?)", cr).Error; err != nil {
|
||||
return nil, newGormError("%v", err)
|
||||
}
|
||||
|
||||
singleton.OnDeleteCron(cr)
|
||||
singleton.UpdateCronList()
|
||||
return nil, nil
|
||||
}
|
202
cmd/dashboard/controller/ddns.go
Normal file
202
cmd/dashboard/controller/ddns.go
Normal file
@ -0,0 +1,202 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jinzhu/copier"
|
||||
"golang.org/x/net/idna"
|
||||
|
||||
"github.com/nezhahq/nezha/model"
|
||||
"github.com/nezhahq/nezha/service/singleton"
|
||||
)
|
||||
|
||||
// List DDNS Profiles
|
||||
// @Summary List DDNS profiles
|
||||
// @Schemes
|
||||
// @Description List DDNS profiles
|
||||
// @Security BearerAuth
|
||||
// @Tags auth required
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[[]model.DDNSProfile]
|
||||
// @Router /ddns [get]
|
||||
func listDDNS(c *gin.Context) ([]*model.DDNSProfile, error) {
|
||||
var ddnsProfiles []*model.DDNSProfile
|
||||
|
||||
singleton.DDNSCacheLock.RLock()
|
||||
defer singleton.DDNSCacheLock.RUnlock()
|
||||
|
||||
if err := copier.Copy(&ddnsProfiles, &singleton.DDNSList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ddnsProfiles, nil
|
||||
}
|
||||
|
||||
// Add DDNS profile
|
||||
// @Summary Add DDNS profile
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description Add DDNS profile
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @param request body model.DDNSForm true "DDNS Request"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[uint64]
|
||||
// @Router /ddns [post]
|
||||
func createDDNS(c *gin.Context) (uint64, error) {
|
||||
var df model.DDNSForm
|
||||
var p model.DDNSProfile
|
||||
|
||||
if err := c.ShouldBindJSON(&df); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if df.MaxRetries < 1 || df.MaxRetries > 10 {
|
||||
return 0, singleton.Localizer.ErrorT("the retry count must be an integer between 1 and 10")
|
||||
}
|
||||
|
||||
p.Name = df.Name
|
||||
enableIPv4 := df.EnableIPv4
|
||||
enableIPv6 := df.EnableIPv6
|
||||
p.EnableIPv4 = &enableIPv4
|
||||
p.EnableIPv6 = &enableIPv6
|
||||
p.MaxRetries = df.MaxRetries
|
||||
p.Provider = df.Provider
|
||||
p.Domains = df.Domains
|
||||
p.AccessID = df.AccessID
|
||||
p.AccessSecret = df.AccessSecret
|
||||
p.WebhookURL = df.WebhookURL
|
||||
p.WebhookMethod = df.WebhookMethod
|
||||
p.WebhookRequestType = df.WebhookRequestType
|
||||
p.WebhookRequestBody = df.WebhookRequestBody
|
||||
p.WebhookHeaders = df.WebhookHeaders
|
||||
|
||||
for n, domain := range p.Domains {
|
||||
// IDN to ASCII
|
||||
domainValid, domainErr := idna.Lookup.ToASCII(domain)
|
||||
if domainErr != nil {
|
||||
return 0, singleton.Localizer.ErrorT("error parsing %s: %v", domain, domainErr)
|
||||
}
|
||||
p.Domains[n] = domainValid
|
||||
}
|
||||
|
||||
if err := singleton.DB.Create(&p).Error; err != nil {
|
||||
return 0, newGormError("%v", err)
|
||||
}
|
||||
|
||||
singleton.OnDDNSUpdate(&p)
|
||||
singleton.UpdateDDNSList()
|
||||
|
||||
return p.ID, nil
|
||||
}
|
||||
|
||||
// Edit DDNS profile
|
||||
// @Summary Edit DDNS profile
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description Edit DDNS profile
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @param id path uint true "Profile ID"
|
||||
// @param request body model.DDNSForm true "DDNS Request"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[any]
|
||||
// @Router /ddns/{id} [patch]
|
||||
func updateDDNS(c *gin.Context) (any, error) {
|
||||
idStr := c.Param("id")
|
||||
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var df model.DDNSForm
|
||||
if err := c.ShouldBindJSON(&df); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if df.MaxRetries < 1 || df.MaxRetries > 10 {
|
||||
return nil, singleton.Localizer.ErrorT("the retry count must be an integer between 1 and 10")
|
||||
}
|
||||
|
||||
var p model.DDNSProfile
|
||||
if err = singleton.DB.First(&p, id).Error; err != nil {
|
||||
return nil, singleton.Localizer.ErrorT("profile id %d does not exist", id)
|
||||
}
|
||||
|
||||
p.Name = df.Name
|
||||
enableIPv4 := df.EnableIPv4
|
||||
enableIPv6 := df.EnableIPv6
|
||||
p.EnableIPv4 = &enableIPv4
|
||||
p.EnableIPv6 = &enableIPv6
|
||||
p.MaxRetries = df.MaxRetries
|
||||
p.Provider = df.Provider
|
||||
p.Domains = df.Domains
|
||||
p.AccessID = df.AccessID
|
||||
p.AccessSecret = df.AccessSecret
|
||||
p.WebhookURL = df.WebhookURL
|
||||
p.WebhookMethod = df.WebhookMethod
|
||||
p.WebhookRequestType = df.WebhookRequestType
|
||||
p.WebhookRequestBody = df.WebhookRequestBody
|
||||
p.WebhookHeaders = df.WebhookHeaders
|
||||
|
||||
for n, domain := range p.Domains {
|
||||
// IDN to ASCII
|
||||
domainValid, domainErr := idna.Lookup.ToASCII(domain)
|
||||
if domainErr != nil {
|
||||
return nil, singleton.Localizer.ErrorT("error parsing %s: %v", domain, domainErr)
|
||||
}
|
||||
p.Domains[n] = domainValid
|
||||
}
|
||||
|
||||
if err = singleton.DB.Save(&p).Error; err != nil {
|
||||
return nil, newGormError("%v", err)
|
||||
}
|
||||
|
||||
singleton.OnDDNSUpdate(&p)
|
||||
singleton.UpdateDDNSList()
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Batch delete DDNS configurations
|
||||
// @Summary Batch delete DDNS configurations
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description Batch delete DDNS configurations
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @param request body []uint64 true "id list"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[any]
|
||||
// @Router /batch-delete/ddns [post]
|
||||
func batchDeleteDDNS(c *gin.Context) (any, error) {
|
||||
var ddnsConfigs []uint64
|
||||
|
||||
if err := c.ShouldBindJSON(&ddnsConfigs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := singleton.DB.Unscoped().Delete(&model.DDNSProfile{}, "id in (?)", ddnsConfigs).Error; err != nil {
|
||||
return nil, newGormError("%v", err)
|
||||
}
|
||||
|
||||
singleton.OnDDNSDelete(ddnsConfigs)
|
||||
singleton.UpdateDDNSList()
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// List DDNS Providers
|
||||
// @Summary List DDNS providers
|
||||
// @Schemes
|
||||
// @Description List DDNS providers
|
||||
// @Security BearerAuth
|
||||
// @Tags auth required
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[[]string]
|
||||
// @Router /ddns/providers [get]
|
||||
func listProviders(c *gin.Context) ([]string, error) {
|
||||
return model.ProviderList, nil
|
||||
}
|
103
cmd/dashboard/controller/fm.go
Normal file
103
cmd/dashboard/controller/fm.go
Normal file
@ -0,0 +1,103 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/hashicorp/go-uuid"
|
||||
"github.com/nezhahq/nezha/model"
|
||||
"github.com/nezhahq/nezha/pkg/utils"
|
||||
"github.com/nezhahq/nezha/pkg/websocketx"
|
||||
"github.com/nezhahq/nezha/proto"
|
||||
"github.com/nezhahq/nezha/service/rpc"
|
||||
"github.com/nezhahq/nezha/service/singleton"
|
||||
)
|
||||
|
||||
// Create FM session
|
||||
// @Summary Create FM session
|
||||
// @Description Create an "attached" FM. It is advised to only call this within a terminal session.
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @Param id query uint true "Server ID"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CreateFMResponse
|
||||
// @Router /file [get]
|
||||
func createFM(c *gin.Context) (*model.CreateFMResponse, error) {
|
||||
idStr := c.Query("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
streamId, err := uuid.GenerateUUID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rpc.NezhaHandlerSingleton.CreateStream(streamId)
|
||||
|
||||
singleton.ServerLock.RLock()
|
||||
server := singleton.ServerList[id]
|
||||
singleton.ServerLock.RUnlock()
|
||||
if server == nil || server.TaskStream == nil {
|
||||
return nil, singleton.Localizer.ErrorT("server not found or not connected")
|
||||
}
|
||||
|
||||
fmData, _ := utils.Json.Marshal(&model.TaskFM{
|
||||
StreamID: streamId,
|
||||
})
|
||||
if err := server.TaskStream.Send(&proto.Task{
|
||||
Type: model.TaskTypeFM,
|
||||
Data: string(fmData),
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.CreateFMResponse{
|
||||
SessionID: streamId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Start FM stream
|
||||
// @Summary Start FM stream
|
||||
// @Description Start FM stream
|
||||
// @Tags auth required
|
||||
// @Param id path string true "Stream UUID"
|
||||
// @Success 200 {object} model.CommonResponse[any]
|
||||
// @Router /ws/file/{id} [get]
|
||||
func fmStream(c *gin.Context) (any, error) {
|
||||
streamId := c.Param("id")
|
||||
if _, err := rpc.NezhaHandlerSingleton.GetStream(streamId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rpc.NezhaHandlerSingleton.CloseStream(streamId)
|
||||
|
||||
wsConn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
return nil, newWsError("%v", err)
|
||||
}
|
||||
defer wsConn.Close()
|
||||
conn := websocketx.NewConn(wsConn)
|
||||
|
||||
go func() {
|
||||
// PING 保活
|
||||
for {
|
||||
if err = conn.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
|
||||
return
|
||||
}
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}()
|
||||
|
||||
if err = rpc.NezhaHandlerSingleton.UserConnected(streamId, conn); err != nil {
|
||||
return nil, newWsError("%v", err)
|
||||
}
|
||||
|
||||
if err = rpc.NezhaHandlerSingleton.StartStream(streamId, time.Second*10); err != nil {
|
||||
return nil, newWsError("%v", err)
|
||||
}
|
||||
|
||||
return nil, newWsError("")
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
|
||||
"github.com/naiba/nezha/model"
|
||||
"github.com/naiba/nezha/pkg/mygin"
|
||||
"github.com/naiba/nezha/service/singleton"
|
||||
)
|
||||
|
||||
type guestPage struct {
|
||||
r *gin.Engine
|
||||
}
|
||||
|
||||
func (gp *guestPage) serve() {
|
||||
gr := gp.r.Group("")
|
||||
gr.Use(mygin.Authorize(mygin.AuthorizeOption{
|
||||
GuestOnly: true,
|
||||
IsPage: true,
|
||||
Msg: "您已登录",
|
||||
Btn: "返回首页",
|
||||
Redirect: "/",
|
||||
}))
|
||||
|
||||
gr.GET("/login", gp.login)
|
||||
|
||||
oauth := &oauth2controller{
|
||||
r: gr,
|
||||
}
|
||||
oauth.serve()
|
||||
}
|
||||
|
||||
func (gp *guestPage) login(c *gin.Context) {
|
||||
if singleton.Conf.Oauth2.OidcAutoLogin {
|
||||
c.Redirect(http.StatusFound, "/oauth2/login")
|
||||
return
|
||||
}
|
||||
LoginType := "GitHub"
|
||||
RegistrationLink := "https://github.com/join"
|
||||
if singleton.Conf.Oauth2.Type == model.ConfigTypeGitee {
|
||||
LoginType = "Gitee"
|
||||
RegistrationLink = "https://gitee.com/signup"
|
||||
} else if singleton.Conf.Oauth2.Type == model.ConfigTypeGitlab {
|
||||
LoginType = "Gitlab"
|
||||
RegistrationLink = "https://gitlab.com/users/sign_up"
|
||||
} else if singleton.Conf.Oauth2.Type == model.ConfigTypeJihulab {
|
||||
LoginType = "Jihulab"
|
||||
RegistrationLink = "https://jihulab.com/users/sign_up"
|
||||
} else if singleton.Conf.Oauth2.Type == model.ConfigTypeGitea {
|
||||
LoginType = "Gitea"
|
||||
RegistrationLink = fmt.Sprintf("%s/user/sign_up", singleton.Conf.Oauth2.Endpoint)
|
||||
} else if singleton.Conf.Oauth2.Type == model.ConfigTypeCloudflare {
|
||||
LoginType = "Cloudflare"
|
||||
RegistrationLink = "https://dash.cloudflare.com/sign-up/teams"
|
||||
} else if singleton.Conf.Oauth2.Type == model.ConfigTypeOidc {
|
||||
LoginType = singleton.Conf.Oauth2.OidcDisplayName
|
||||
RegistrationLink = singleton.Conf.Oauth2.OidcRegisterURL
|
||||
}
|
||||
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/login", mygin.CommonEnvironment(c, gin.H{
|
||||
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Login"}),
|
||||
"LoginType": LoginType,
|
||||
"RegistrationLink": RegistrationLink,
|
||||
}))
|
||||
}
|
180
cmd/dashboard/controller/jwt.go
Normal file
180
cmd/dashboard/controller/jwt.go
Normal file
@ -0,0 +1,180 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
jwt "github.com/appleboy/gin-jwt/v2"
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/nezhahq/nezha/cmd/dashboard/controller/waf"
|
||||
"github.com/nezhahq/nezha/model"
|
||||
"github.com/nezhahq/nezha/pkg/utils"
|
||||
"github.com/nezhahq/nezha/service/singleton"
|
||||
)
|
||||
|
||||
func initParams() *jwt.GinJWTMiddleware {
|
||||
return &jwt.GinJWTMiddleware{
|
||||
Realm: singleton.Conf.SiteName,
|
||||
Key: []byte(singleton.Conf.JWTSecretKey),
|
||||
CookieName: "nz-jwt",
|
||||
SendCookie: true,
|
||||
Timeout: time.Hour,
|
||||
MaxRefresh: time.Hour,
|
||||
IdentityKey: model.CtxKeyAuthorizedUser,
|
||||
PayloadFunc: payloadFunc(),
|
||||
|
||||
IdentityHandler: identityHandler(),
|
||||
Authenticator: authenticator(),
|
||||
Authorizator: authorizator(),
|
||||
Unauthorized: unauthorized(),
|
||||
TokenLookup: "header: Authorization, query: token, cookie: nz-jwt",
|
||||
TokenHeadName: "Bearer",
|
||||
TimeFunc: time.Now,
|
||||
|
||||
LoginResponse: func(c *gin.Context, code int, token string, expire time.Time) {
|
||||
c.JSON(http.StatusOK, model.CommonResponse[model.LoginResponse]{
|
||||
Success: true,
|
||||
Data: model.LoginResponse{
|
||||
Token: token,
|
||||
Expire: expire.Format(time.RFC3339),
|
||||
},
|
||||
})
|
||||
},
|
||||
RefreshResponse: refreshResponse,
|
||||
}
|
||||
}
|
||||
|
||||
func payloadFunc() func(data interface{}) jwt.MapClaims {
|
||||
return func(data interface{}) jwt.MapClaims {
|
||||
if v, ok := data.(string); ok {
|
||||
return jwt.MapClaims{
|
||||
model.CtxKeyAuthorizedUser: v,
|
||||
}
|
||||
}
|
||||
return jwt.MapClaims{}
|
||||
}
|
||||
}
|
||||
|
||||
func identityHandler() func(c *gin.Context) interface{} {
|
||||
return func(c *gin.Context) interface{} {
|
||||
claims := jwt.ExtractClaims(c)
|
||||
userId := claims[model.CtxKeyAuthorizedUser].(string)
|
||||
var user model.User
|
||||
if err := singleton.DB.First(&user, userId).Error; err != nil {
|
||||
return nil
|
||||
}
|
||||
return &user
|
||||
}
|
||||
}
|
||||
|
||||
// User Login
|
||||
// @Summary user login
|
||||
// @Schemes
|
||||
// @Description user login
|
||||
// @Accept json
|
||||
// @param loginRequest body model.LoginRequest true "Login Request"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[model.LoginResponse]
|
||||
// @Router /login [post]
|
||||
func authenticator() func(c *gin.Context) (interface{}, error) {
|
||||
return func(c *gin.Context) (interface{}, error) {
|
||||
var loginVals model.LoginRequest
|
||||
if err := c.ShouldBind(&loginVals); err != nil {
|
||||
return "", jwt.ErrMissingLoginValues
|
||||
}
|
||||
|
||||
var user model.User
|
||||
if err := singleton.DB.Select("id", "password").Where("username = ?", loginVals.Username).First(&user).Error; err != nil {
|
||||
model.BlockIP(singleton.DB, c.GetString(model.CtxKeyRealIPStr), model.WAFBlockReasonTypeLoginFail)
|
||||
return nil, jwt.ErrFailedAuthentication
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(loginVals.Password)); err != nil {
|
||||
model.BlockIP(singleton.DB, c.GetString(model.CtxKeyRealIPStr), model.WAFBlockReasonTypeLoginFail)
|
||||
return nil, jwt.ErrFailedAuthentication
|
||||
}
|
||||
|
||||
return utils.Itoa(user.ID), nil
|
||||
}
|
||||
}
|
||||
|
||||
func authorizator() func(data interface{}, c *gin.Context) bool {
|
||||
return func(data interface{}, c *gin.Context) bool {
|
||||
_, ok := data.(*model.User)
|
||||
return ok
|
||||
}
|
||||
}
|
||||
|
||||
func unauthorized() func(c *gin.Context, code int, message string) {
|
||||
return func(c *gin.Context, code int, message string) {
|
||||
c.JSON(http.StatusOK, model.CommonResponse[any]{
|
||||
Success: false,
|
||||
Error: "ApiErrorUnauthorized",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh token
|
||||
// @Summary Refresh token
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description Refresh token
|
||||
// @Tags auth required
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[model.LoginResponse]
|
||||
// @Router /refresh-token [get]
|
||||
func refreshResponse(c *gin.Context, code int, token string, expire time.Time) {
|
||||
c.JSON(http.StatusOK, model.CommonResponse[model.LoginResponse]{
|
||||
Success: true,
|
||||
Data: model.LoginResponse{
|
||||
Token: token,
|
||||
Expire: expire.Format(time.RFC3339),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func optionalAuthMiddleware(mw *jwt.GinJWTMiddleware) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
claims, err := mw.GetClaimsFromJWT(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch v := claims["exp"].(type) {
|
||||
case nil:
|
||||
return
|
||||
case float64:
|
||||
if int64(v) < mw.TimeFunc().Unix() {
|
||||
return
|
||||
}
|
||||
case json.Number:
|
||||
n, err := v.Int64()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if n < mw.TimeFunc().Unix() {
|
||||
return
|
||||
}
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("JWT_PAYLOAD", claims)
|
||||
identity := mw.IdentityHandler(c)
|
||||
|
||||
if identity != nil {
|
||||
model.ClearIP(singleton.DB, c.GetString(model.CtxKeyRealIPStr))
|
||||
c.Set(mw.IdentityKey, identity)
|
||||
} else {
|
||||
if err := model.BlockIP(singleton.DB, c.GetString(model.CtxKeyRealIPStr), model.WAFBlockReasonTypeBruteForceToken); err != nil {
|
||||
waf.ShowBlockPage(c, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,108 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/naiba/nezha/model"
|
||||
"github.com/naiba/nezha/pkg/mygin"
|
||||
"github.com/naiba/nezha/service/singleton"
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
)
|
||||
|
||||
type memberPage struct {
|
||||
r *gin.Engine
|
||||
}
|
||||
|
||||
func (mp *memberPage) serve() {
|
||||
mr := mp.r.Group("")
|
||||
mr.Use(mygin.Authorize(mygin.AuthorizeOption{
|
||||
MemberOnly: true,
|
||||
IsPage: true,
|
||||
Msg: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "YouAreNotAuthorized"}),
|
||||
Btn: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Login"}),
|
||||
Redirect: "/login",
|
||||
}))
|
||||
mr.GET("/server", mp.server)
|
||||
mr.GET("/monitor", mp.monitor)
|
||||
mr.GET("/cron", mp.cron)
|
||||
mr.GET("/notification", mp.notification)
|
||||
mr.GET("/ddns", mp.ddns)
|
||||
mr.GET("/nat", mp.nat)
|
||||
mr.GET("/setting", mp.setting)
|
||||
mr.GET("/api", mp.api)
|
||||
}
|
||||
|
||||
func (mp *memberPage) api(c *gin.Context) {
|
||||
singleton.ApiLock.RLock()
|
||||
defer singleton.ApiLock.RUnlock()
|
||||
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/api", mygin.CommonEnvironment(c, gin.H{
|
||||
"title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ApiManagement"}),
|
||||
"Tokens": singleton.ApiTokenList,
|
||||
}))
|
||||
}
|
||||
|
||||
func (mp *memberPage) server(c *gin.Context) {
|
||||
singleton.SortedServerLock.RLock()
|
||||
defer singleton.SortedServerLock.RUnlock()
|
||||
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/server", mygin.CommonEnvironment(c, gin.H{
|
||||
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ServersManagement"}),
|
||||
"Servers": singleton.SortedServerList,
|
||||
}))
|
||||
}
|
||||
|
||||
func (mp *memberPage) monitor(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/monitor", mygin.CommonEnvironment(c, gin.H{
|
||||
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ServicesManagement"}),
|
||||
"Monitors": singleton.ServiceSentinelShared.Monitors(),
|
||||
}))
|
||||
}
|
||||
|
||||
func (mp *memberPage) cron(c *gin.Context) {
|
||||
var crons []model.Cron
|
||||
singleton.DB.Find(&crons)
|
||||
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/cron", mygin.CommonEnvironment(c, gin.H{
|
||||
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ScheduledTasks"}),
|
||||
"Crons": crons,
|
||||
}))
|
||||
}
|
||||
|
||||
func (mp *memberPage) notification(c *gin.Context) {
|
||||
var nf []model.Notification
|
||||
singleton.DB.Find(&nf)
|
||||
var ar []model.AlertRule
|
||||
singleton.DB.Find(&ar)
|
||||
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/notification", mygin.CommonEnvironment(c, gin.H{
|
||||
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Notification"}),
|
||||
"Notifications": nf,
|
||||
"AlertRules": ar,
|
||||
}))
|
||||
}
|
||||
|
||||
func (mp *memberPage) ddns(c *gin.Context) {
|
||||
var data []model.DDNSProfile
|
||||
singleton.DB.Find(&data)
|
||||
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/ddns", mygin.CommonEnvironment(c, gin.H{
|
||||
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "DDNS"}),
|
||||
"DDNS": data,
|
||||
"ProviderMap": model.ProviderMap,
|
||||
"ProviderList": model.ProviderList,
|
||||
}))
|
||||
}
|
||||
|
||||
func (mp *memberPage) nat(c *gin.Context) {
|
||||
var data []model.NAT
|
||||
singleton.DB.Find(&data)
|
||||
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/nat", mygin.CommonEnvironment(c, gin.H{
|
||||
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "NAT"}),
|
||||
"NAT": data,
|
||||
}))
|
||||
}
|
||||
|
||||
func (mp *memberPage) setting(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/setting", mygin.CommonEnvironment(c, gin.H{
|
||||
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Settings"}),
|
||||
"Languages": model.Languages,
|
||||
"DashboardThemes": model.DashboardThemes,
|
||||
}))
|
||||
}
|
137
cmd/dashboard/controller/nat.go
Normal file
137
cmd/dashboard/controller/nat.go
Normal file
@ -0,0 +1,137 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jinzhu/copier"
|
||||
|
||||
"github.com/nezhahq/nezha/model"
|
||||
"github.com/nezhahq/nezha/service/singleton"
|
||||
)
|
||||
|
||||
// List NAT Profiles
|
||||
// @Summary List NAT profiles
|
||||
// @Schemes
|
||||
// @Description List NAT profiles
|
||||
// @Security BearerAuth
|
||||
// @Tags auth required
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[[]model.NAT]
|
||||
// @Router /nat [get]
|
||||
func listNAT(c *gin.Context) ([]*model.NAT, error) {
|
||||
var n []*model.NAT
|
||||
|
||||
singleton.NATCacheRwLock.RLock()
|
||||
defer singleton.NATCacheRwLock.RUnlock()
|
||||
|
||||
if err := copier.Copy(&n, &singleton.NATList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Add NAT profile
|
||||
// @Summary Add NAT profile
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description Add NAT profile
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @param request body model.NATForm true "NAT Request"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[uint64]
|
||||
// @Router /nat [post]
|
||||
func createNAT(c *gin.Context) (uint64, error) {
|
||||
var nf model.NATForm
|
||||
var n model.NAT
|
||||
|
||||
if err := c.ShouldBindJSON(&nf); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
n.Name = nf.Name
|
||||
n.Domain = nf.Domain
|
||||
n.Host = nf.Host
|
||||
n.ServerID = nf.ServerID
|
||||
|
||||
if err := singleton.DB.Create(&n).Error; err != nil {
|
||||
return 0, newGormError("%v", err)
|
||||
}
|
||||
|
||||
singleton.OnNATUpdate(&n)
|
||||
singleton.UpdateNATList()
|
||||
return n.ID, nil
|
||||
}
|
||||
|
||||
// Edit NAT profile
|
||||
// @Summary Edit NAT profile
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description Edit NAT profile
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @param id path uint true "Profile ID"
|
||||
// @param request body model.NATForm true "NAT Request"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[any]
|
||||
// @Router /nat/{id} [patch]
|
||||
func updateNAT(c *gin.Context) (any, error) {
|
||||
idStr := c.Param("id")
|
||||
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var nf model.NATForm
|
||||
if err := c.ShouldBindJSON(&nf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var n model.NAT
|
||||
if err = singleton.DB.First(&n, id).Error; err != nil {
|
||||
return nil, singleton.Localizer.ErrorT("profile id %d does not exist", id)
|
||||
}
|
||||
|
||||
n.Name = nf.Name
|
||||
n.Domain = nf.Domain
|
||||
n.Host = nf.Host
|
||||
n.ServerID = nf.ServerID
|
||||
|
||||
if err := singleton.DB.Save(&n).Error; err != nil {
|
||||
return 0, newGormError("%v", err)
|
||||
}
|
||||
|
||||
singleton.OnNATUpdate(&n)
|
||||
singleton.UpdateNATList()
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Batch delete NAT configurations
|
||||
// @Summary Batch delete NAT configurations
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description Batch delete NAT configurations
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @param request body []uint64 true "id list"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[any]
|
||||
// @Router /batch-delete/nat [post]
|
||||
func batchDeleteNAT(c *gin.Context) (any, error) {
|
||||
var n []uint64
|
||||
|
||||
if err := c.ShouldBindJSON(&n); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := singleton.DB.Unscoped().Delete(&model.NAT{}, "id in (?)", n).Error; err != nil {
|
||||
return nil, newGormError("%v", err)
|
||||
}
|
||||
|
||||
singleton.OnNATDelete(n)
|
||||
singleton.UpdateNATList()
|
||||
return nil, nil
|
||||
}
|
174
cmd/dashboard/controller/notification.go
Normal file
174
cmd/dashboard/controller/notification.go
Normal file
@ -0,0 +1,174 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/nezhahq/nezha/model"
|
||||
"github.com/nezhahq/nezha/service/singleton"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// List notification
|
||||
// @Summary List notification
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description List notification
|
||||
// @Tags auth required
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[[]model.Notification]
|
||||
// @Router /notification [get]
|
||||
func listNotification(c *gin.Context) ([]*model.Notification, error) {
|
||||
singleton.NotificationsLock.RLock()
|
||||
defer singleton.NotificationsLock.RUnlock()
|
||||
|
||||
var notifications []*model.Notification
|
||||
if err := copier.Copy(¬ifications, &singleton.NotificationListSorted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return notifications, nil
|
||||
}
|
||||
|
||||
// Add notification
|
||||
// @Summary Add notification
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description Add notification
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @param request body model.NotificationForm true "NotificationForm"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[any]
|
||||
// @Router /notification [post]
|
||||
func createNotification(c *gin.Context) (uint64, error) {
|
||||
var nf model.NotificationForm
|
||||
if err := c.ShouldBindJSON(&nf); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var n model.Notification
|
||||
n.Name = nf.Name
|
||||
n.RequestMethod = nf.RequestMethod
|
||||
n.RequestType = nf.RequestType
|
||||
n.RequestHeader = nf.RequestHeader
|
||||
n.RequestBody = nf.RequestBody
|
||||
n.URL = nf.URL
|
||||
verifyTLS := nf.VerifyTLS
|
||||
n.VerifyTLS = &verifyTLS
|
||||
|
||||
ns := model.NotificationServerBundle{
|
||||
Notification: &n,
|
||||
Server: nil,
|
||||
Loc: singleton.Loc,
|
||||
}
|
||||
// 未勾选跳过检查
|
||||
if !nf.SkipCheck {
|
||||
if err := ns.Send(singleton.Localizer.T("a test message")); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := singleton.DB.Create(&n).Error; err != nil {
|
||||
return 0, newGormError("%v", err)
|
||||
}
|
||||
|
||||
singleton.OnRefreshOrAddNotification(&n)
|
||||
singleton.UpdateNotificationList()
|
||||
return n.ID, nil
|
||||
}
|
||||
|
||||
// Edit notification
|
||||
// @Summary Edit notification
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description Edit notification
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @Param id path uint true "Notification ID"
|
||||
// @Param body body model.NotificationForm true "NotificationForm"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[any]
|
||||
// @Router /notification/{id} [patch]
|
||||
func updateNotification(c *gin.Context) (any, error) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var nf model.NotificationForm
|
||||
if err := c.ShouldBindJSON(&nf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var n model.Notification
|
||||
if err := singleton.DB.First(&n, id).Error; err != nil {
|
||||
return nil, singleton.Localizer.ErrorT("notification id %d does not exist", id)
|
||||
}
|
||||
|
||||
n.Name = nf.Name
|
||||
n.RequestMethod = nf.RequestMethod
|
||||
n.RequestType = nf.RequestType
|
||||
n.RequestHeader = nf.RequestHeader
|
||||
n.RequestBody = nf.RequestBody
|
||||
n.URL = nf.URL
|
||||
verifyTLS := nf.VerifyTLS
|
||||
n.VerifyTLS = &verifyTLS
|
||||
|
||||
ns := model.NotificationServerBundle{
|
||||
Notification: &n,
|
||||
Server: nil,
|
||||
Loc: singleton.Loc,
|
||||
}
|
||||
// 未勾选跳过检查
|
||||
if !nf.SkipCheck {
|
||||
if err := ns.Send(singleton.Localizer.T("a test message")); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := singleton.DB.Save(&n).Error; err != nil {
|
||||
return nil, newGormError("%v", err)
|
||||
}
|
||||
|
||||
singleton.OnRefreshOrAddNotification(&n)
|
||||
singleton.UpdateNotificationList()
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Batch delete notifications
|
||||
// @Summary Batch delete notifications
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description Batch delete notifications
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @param request body []uint64 true "id list"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[any]
|
||||
// @Router /batch-delete/notification [post]
|
||||
func batchDeleteNotification(c *gin.Context) (any, error) {
|
||||
var n []uint64
|
||||
|
||||
if err := c.ShouldBindJSON(&n); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err := singleton.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Unscoped().Delete(&model.Notification{}, "id in (?)", n).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Unscoped().Delete(&model.NotificationGroupNotification{}, "notification_id in (?)", n).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, newGormError("%v", err)
|
||||
}
|
||||
|
||||
singleton.OnDeleteNotification(n)
|
||||
singleton.UpdateNotificationList()
|
||||
return nil, nil
|
||||
}
|
204
cmd/dashboard/controller/notification_group.go
Normal file
204
cmd/dashboard/controller/notification_group.go
Normal file
@ -0,0 +1,204 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/nezhahq/nezha/model"
|
||||
"github.com/nezhahq/nezha/service/singleton"
|
||||
)
|
||||
|
||||
// List notification group
|
||||
// @Summary List notification group
|
||||
// @Schemes
|
||||
// @Description List notification group
|
||||
// @Security BearerAuth
|
||||
// @Tags auth required
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[[]model.NotificationGroupResponseItem]
|
||||
// @Router /notification-group [get]
|
||||
func listNotificationGroup(c *gin.Context) ([]model.NotificationGroupResponseItem, error) {
|
||||
var ng []model.NotificationGroup
|
||||
if err := singleton.DB.Find(&ng).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ngn []model.NotificationGroupNotification
|
||||
if err := singleton.DB.Find(&ngn).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
groupNotifications := make(map[uint64][]uint64, len(ng))
|
||||
for _, n := range ngn {
|
||||
if _, ok := groupNotifications[n.NotificationGroupID]; !ok {
|
||||
groupNotifications[n.NotificationGroupID] = make([]uint64, 0)
|
||||
}
|
||||
groupNotifications[n.NotificationGroupID] = append(groupNotifications[n.NotificationGroupID], n.NotificationID)
|
||||
}
|
||||
|
||||
ngRes := make([]model.NotificationGroupResponseItem, 0, len(ng))
|
||||
for _, n := range ng {
|
||||
ngRes = append(ngRes, model.NotificationGroupResponseItem{
|
||||
Group: n,
|
||||
Notifications: groupNotifications[n.ID],
|
||||
})
|
||||
}
|
||||
|
||||
return ngRes, nil
|
||||
}
|
||||
|
||||
// New notification group
|
||||
// @Summary New notification group
|
||||
// @Schemes
|
||||
// @Description New notification group
|
||||
// @Security BearerAuth
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @Param body body model.NotificationGroupForm true "NotificationGroupForm"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[any]
|
||||
// @Router /notification-group [post]
|
||||
func createNotificationGroup(c *gin.Context) (uint64, error) {
|
||||
var ngf model.NotificationGroupForm
|
||||
if err := c.ShouldBindJSON(&ngf); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
ngf.Notifications = slices.Compact(ngf.Notifications)
|
||||
|
||||
var ng model.NotificationGroup
|
||||
ng.Name = ngf.Name
|
||||
|
||||
var count int64
|
||||
if err := singleton.DB.Model(&model.Notification{}).Where("id in (?)", ngf.Notifications).Count(&count).Error; err != nil {
|
||||
return 0, newGormError("%v", err)
|
||||
}
|
||||
|
||||
if count != int64(len(ngf.Notifications)) {
|
||||
return 0, singleton.Localizer.ErrorT("have invalid notification id")
|
||||
}
|
||||
|
||||
err := singleton.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Create(&ng).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, n := range ngf.Notifications {
|
||||
if err := tx.Create(&model.NotificationGroupNotification{
|
||||
NotificationGroupID: ng.ID,
|
||||
NotificationID: n,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return 0, newGormError("%v", err)
|
||||
}
|
||||
|
||||
singleton.OnRefreshOrAddNotificationGroup(&ng, ngf.Notifications)
|
||||
return ng.ID, nil
|
||||
}
|
||||
|
||||
// Edit notification group
|
||||
// @Summary Edit notification group
|
||||
// @Schemes
|
||||
// @Description Edit notification group
|
||||
// @Security BearerAuth
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @Param id path uint true "ID"
|
||||
// @Param body body model.NotificationGroupForm true "NotificationGroupForm"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[any]
|
||||
// @Router /notification-group/{id} [patch]
|
||||
func updateNotificationGroup(c *gin.Context) (any, error) {
|
||||
idStr := c.Param("id")
|
||||
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ngf model.NotificationGroupForm
|
||||
if err := c.ShouldBindJSON(&ngf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var ngDB model.NotificationGroup
|
||||
if err := singleton.DB.First(&ngDB, id).Error; err != nil {
|
||||
return nil, singleton.Localizer.ErrorT("group id %d does not exist", id)
|
||||
}
|
||||
|
||||
ngDB.Name = ngf.Name
|
||||
ngf.Notifications = slices.Compact(ngf.Notifications)
|
||||
|
||||
var count int64
|
||||
if err := singleton.DB.Model(&model.Server{}).Where("id in (?)", ngf.Notifications).Count(&count).Error; err != nil {
|
||||
return nil, newGormError("%v", err)
|
||||
}
|
||||
if count != int64(len(ngf.Notifications)) {
|
||||
return nil, singleton.Localizer.ErrorT("have invalid notification id")
|
||||
}
|
||||
|
||||
err = singleton.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Save(&ngDB).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Unscoped().Delete(&model.NotificationGroupNotification{}, "notification_group_id = ?", id).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, n := range ngf.Notifications {
|
||||
if err := tx.Create(&model.NotificationGroupNotification{
|
||||
NotificationGroupID: ngDB.ID,
|
||||
NotificationID: n,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, newGormError("%v", err)
|
||||
}
|
||||
|
||||
singleton.OnRefreshOrAddNotificationGroup(&ngDB, ngf.Notifications)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Batch delete notification group
|
||||
// @Summary Batch delete notification group
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description Batch delete notification group
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @param request body []uint64 true "id list"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[any]
|
||||
// @Router /batch-delete/notification-group [post]
|
||||
func batchDeleteNotificationGroup(c *gin.Context) (any, error) {
|
||||
var ngn []uint64
|
||||
if err := c.ShouldBindJSON(&ngn); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err := singleton.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Unscoped().Delete(&model.NotificationGroup{}, "id in (?)", ngn).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Unscoped().Delete(&model.NotificationGroupNotification{}, "notification_group_id in (?)", ngn).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, newGormError("%v", err)
|
||||
}
|
||||
|
||||
singleton.OnDeleteNotificationGroup(ngn)
|
||||
return nil, nil
|
||||
}
|
@ -1,296 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/naiba/nezha/pkg/oidc/cloudflare"
|
||||
myOidc "github.com/naiba/nezha/pkg/oidc/general"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"github.com/gin-gonic/gin"
|
||||
GitHubAPI "github.com/google/go-github/v47/github"
|
||||
"github.com/naiba/nezha/model"
|
||||
"github.com/naiba/nezha/pkg/mygin"
|
||||
"github.com/naiba/nezha/pkg/utils"
|
||||
"github.com/naiba/nezha/service/singleton"
|
||||
"github.com/patrickmn/go-cache"
|
||||
"github.com/xanzy/go-gitlab"
|
||||
"golang.org/x/oauth2"
|
||||
GitHubOauth2 "golang.org/x/oauth2/github"
|
||||
GitlabOauth2 "golang.org/x/oauth2/gitlab"
|
||||
)
|
||||
|
||||
type oauth2controller struct {
|
||||
r gin.IRoutes
|
||||
oidcProvider *oidc.Provider
|
||||
}
|
||||
|
||||
func (oa *oauth2controller) serve() {
|
||||
oa.r.GET("/oauth2/login", oa.login)
|
||||
oa.r.GET("/oauth2/callback", oa.callback)
|
||||
}
|
||||
|
||||
func (oa *oauth2controller) getCommonOauth2Config(c *gin.Context) *oauth2.Config {
|
||||
if singleton.Conf.Oauth2.Type == model.ConfigTypeGitee {
|
||||
return &oauth2.Config{
|
||||
ClientID: singleton.Conf.Oauth2.ClientID,
|
||||
ClientSecret: singleton.Conf.Oauth2.ClientSecret,
|
||||
Scopes: []string{},
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: "https://gitee.com/oauth/authorize",
|
||||
TokenURL: "https://gitee.com/oauth/token",
|
||||
},
|
||||
RedirectURL: oa.getRedirectURL(c),
|
||||
}
|
||||
} else if singleton.Conf.Oauth2.Type == model.ConfigTypeGitlab {
|
||||
return &oauth2.Config{
|
||||
ClientID: singleton.Conf.Oauth2.ClientID,
|
||||
ClientSecret: singleton.Conf.Oauth2.ClientSecret,
|
||||
Scopes: []string{"read_user", "read_api"},
|
||||
Endpoint: GitlabOauth2.Endpoint,
|
||||
RedirectURL: oa.getRedirectURL(c),
|
||||
}
|
||||
} else if singleton.Conf.Oauth2.Type == model.ConfigTypeJihulab {
|
||||
return &oauth2.Config{
|
||||
ClientID: singleton.Conf.Oauth2.ClientID,
|
||||
ClientSecret: singleton.Conf.Oauth2.ClientSecret,
|
||||
Scopes: []string{"read_user", "read_api"},
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: "https://jihulab.com/oauth/authorize",
|
||||
TokenURL: "https://jihulab.com/oauth/token",
|
||||
},
|
||||
RedirectURL: oa.getRedirectURL(c),
|
||||
}
|
||||
} else if singleton.Conf.Oauth2.Type == model.ConfigTypeGitea {
|
||||
return &oauth2.Config{
|
||||
ClientID: singleton.Conf.Oauth2.ClientID,
|
||||
ClientSecret: singleton.Conf.Oauth2.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: fmt.Sprintf("%s/login/oauth/authorize", singleton.Conf.Oauth2.Endpoint),
|
||||
TokenURL: fmt.Sprintf("%s/login/oauth/access_token", singleton.Conf.Oauth2.Endpoint),
|
||||
},
|
||||
RedirectURL: oa.getRedirectURL(c),
|
||||
}
|
||||
} else if singleton.Conf.Oauth2.Type == model.ConfigTypeCloudflare {
|
||||
return &oauth2.Config{
|
||||
ClientID: singleton.Conf.Oauth2.ClientID,
|
||||
ClientSecret: singleton.Conf.Oauth2.ClientSecret,
|
||||
Scopes: []string{"openid", "email", "profile", "groups"},
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: fmt.Sprintf("%s/cdn-cgi/access/sso/oidc/%s/authorization", singleton.Conf.Oauth2.Endpoint, singleton.Conf.Oauth2.ClientID),
|
||||
TokenURL: fmt.Sprintf("%s/cdn-cgi/access/sso/oidc/%s/token", singleton.Conf.Oauth2.Endpoint, singleton.Conf.Oauth2.ClientID),
|
||||
},
|
||||
RedirectURL: oa.getRedirectURL(c),
|
||||
}
|
||||
} else if singleton.Conf.Oauth2.Type == model.ConfigTypeOidc {
|
||||
var err error
|
||||
oa.oidcProvider, err = oidc.NewProvider(c.Request.Context(), singleton.Conf.Oauth2.OidcIssuer)
|
||||
if err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusBadRequest,
|
||||
Title: fmt.Sprintf("Cannot get OIDC infomaion from issuer from %s", singleton.Conf.Oauth2.OidcIssuer),
|
||||
Msg: err.Error(),
|
||||
}, true)
|
||||
return nil
|
||||
}
|
||||
scopes := strings.Split(singleton.Conf.Oauth2.OidcScopes, ",")
|
||||
scopes = append(scopes, oidc.ScopeOpenID)
|
||||
uniqueScopes := removeDuplicates(scopes)
|
||||
return &oauth2.Config{
|
||||
ClientID: singleton.Conf.Oauth2.ClientID,
|
||||
ClientSecret: singleton.Conf.Oauth2.ClientSecret,
|
||||
Scopes: uniqueScopes,
|
||||
Endpoint: oa.oidcProvider.Endpoint(),
|
||||
RedirectURL: oa.getRedirectURL(c),
|
||||
}
|
||||
} else {
|
||||
return &oauth2.Config{
|
||||
ClientID: singleton.Conf.Oauth2.ClientID,
|
||||
ClientSecret: singleton.Conf.Oauth2.ClientSecret,
|
||||
Scopes: []string{},
|
||||
Endpoint: GitHubOauth2.Endpoint,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (oa *oauth2controller) getRedirectURL(c *gin.Context) string {
|
||||
scheme := "http://"
|
||||
referer := c.Request.Referer()
|
||||
if forwardedProto := c.Request.Header.Get("X-Forwarded-Proto"); forwardedProto == "https" || strings.HasPrefix(referer, "https://") {
|
||||
scheme = "https://"
|
||||
}
|
||||
return scheme + c.Request.Host + "/oauth2/callback"
|
||||
}
|
||||
|
||||
func (oa *oauth2controller) login(c *gin.Context) {
|
||||
randomString, err := utils.GenerateRandomString(32)
|
||||
if err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusBadRequest,
|
||||
Title: "Something Wrong",
|
||||
Msg: err.Error(),
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
state, stateKey := randomString[:16], randomString[16:]
|
||||
singleton.Cache.Set(fmt.Sprintf("%s%s", model.CacheKeyOauth2State, stateKey), state, cache.DefaultExpiration)
|
||||
url := oa.getCommonOauth2Config(c).AuthCodeURL(state, oauth2.AccessTypeOnline)
|
||||
c.SetCookie(singleton.Conf.Site.CookieName+"-sk", stateKey, 60*5, "", "", false, false)
|
||||
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/redirect", mygin.CommonEnvironment(c, gin.H{
|
||||
"URL": url,
|
||||
}))
|
||||
}
|
||||
|
||||
func (oa *oauth2controller) callback(c *gin.Context) {
|
||||
var err error
|
||||
// 验证登录跳转时的 State
|
||||
stateKey, err := c.Cookie(singleton.Conf.Site.CookieName + "-sk")
|
||||
if err == nil {
|
||||
state, ok := singleton.Cache.Get(fmt.Sprintf("%s%s", model.CacheKeyOauth2State, stateKey))
|
||||
if !ok || state.(string) != c.Query("state") {
|
||||
err = errors.New("非法的登录方式")
|
||||
}
|
||||
}
|
||||
oauth2Config := oa.getCommonOauth2Config(c)
|
||||
ctx := context.Background()
|
||||
var otk *oauth2.Token
|
||||
if err == nil {
|
||||
otk, err = oauth2Config.Exchange(ctx, c.Query("code"))
|
||||
}
|
||||
|
||||
var user model.User
|
||||
|
||||
if err == nil {
|
||||
if singleton.Conf.Oauth2.Type == model.ConfigTypeGitlab || singleton.Conf.Oauth2.Type == model.ConfigTypeJihulab {
|
||||
var gitlabApiClient *gitlab.Client
|
||||
if singleton.Conf.Oauth2.Type == model.ConfigTypeGitlab {
|
||||
gitlabApiClient, err = gitlab.NewOAuthClient(otk.AccessToken)
|
||||
} else {
|
||||
gitlabApiClient, err = gitlab.NewOAuthClient(otk.AccessToken, gitlab.WithBaseURL("https://jihulab.com/api/v4/"))
|
||||
}
|
||||
var u *gitlab.User
|
||||
if err == nil {
|
||||
u, _, err = gitlabApiClient.Users.CurrentUser()
|
||||
}
|
||||
if err == nil {
|
||||
user = model.NewUserFromGitlab(u)
|
||||
}
|
||||
} else if singleton.Conf.Oauth2.Type == model.ConfigTypeGitea {
|
||||
var giteaApiClient *gitea.Client
|
||||
giteaApiClient, err = gitea.NewClient(singleton.Conf.Oauth2.Endpoint, gitea.SetToken(otk.AccessToken))
|
||||
var u *gitea.User
|
||||
if err == nil {
|
||||
u, _, err = giteaApiClient.GetMyUserInfo()
|
||||
}
|
||||
if err == nil {
|
||||
user = model.NewUserFromGitea(u)
|
||||
}
|
||||
} else if singleton.Conf.Oauth2.Type == model.ConfigTypeCloudflare {
|
||||
client := oauth2Config.Client(context.Background(), otk)
|
||||
resp, err := client.Get(fmt.Sprintf("%s/cdn-cgi/access/sso/oidc/%s/userinfo", singleton.Conf.Oauth2.Endpoint, singleton.Conf.Oauth2.ClientID))
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
var cloudflareUserInfo *cloudflare.UserInfo
|
||||
if err := utils.Json.NewDecoder(resp.Body).Decode(&cloudflareUserInfo); err == nil {
|
||||
user = cloudflareUserInfo.MapToNezhaUser()
|
||||
}
|
||||
}
|
||||
} else if singleton.Conf.Oauth2.Type == model.ConfigTypeOidc {
|
||||
userInfo, err := oa.oidcProvider.UserInfo(c.Request.Context(), oauth2.StaticTokenSource(otk))
|
||||
if err == nil {
|
||||
loginClaim := singleton.Conf.Oauth2.OidcLoginClaim
|
||||
groupClain := singleton.Conf.Oauth2.OidcGroupClaim
|
||||
adminGroups := strings.Split(singleton.Conf.Oauth2.AdminGroups, ",")
|
||||
autoCreate := singleton.Conf.Oauth2.OidcAutoCreate
|
||||
var oidceUserInfo *myOidc.UserInfo
|
||||
if err := userInfo.Claims(&oidceUserInfo); err == nil {
|
||||
user = oidceUserInfo.MapToNezhaUser(loginClaim, groupClain, adminGroups, autoCreate)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var client *GitHubAPI.Client
|
||||
oc := oauth2Config.Client(ctx, otk)
|
||||
if singleton.Conf.Oauth2.Type == model.ConfigTypeGitee {
|
||||
baseURL, _ := url.Parse("https://gitee.com/api/v5/")
|
||||
uploadURL, _ := url.Parse("https://gitee.com/api/v5/uploads/")
|
||||
client = GitHubAPI.NewClient(oc)
|
||||
client.BaseURL = baseURL
|
||||
client.UploadURL = uploadURL
|
||||
} else {
|
||||
client = GitHubAPI.NewClient(oc)
|
||||
}
|
||||
var gu *GitHubAPI.User
|
||||
gu, _, err = client.Users.Get(ctx, "")
|
||||
if err == nil {
|
||||
user = model.NewUserFromGitHub(gu)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err == nil && user.Login == "" {
|
||||
err = errors.New("获取用户信息失败")
|
||||
}
|
||||
|
||||
if err != nil || user.Login == "" {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusBadRequest,
|
||||
Title: "登录失败",
|
||||
Msg: fmt.Sprintf("错误信息:%s", err),
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
var isAdmin bool
|
||||
|
||||
if user.SuperAdmin {
|
||||
isAdmin = true
|
||||
} else {
|
||||
for _, admin := range strings.Split(singleton.Conf.Oauth2.Admin, ",") {
|
||||
if admin != "" && strings.EqualFold(user.Login, admin) {
|
||||
isAdmin = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !isAdmin {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusBadRequest,
|
||||
Title: "登录失败",
|
||||
Msg: fmt.Sprintf("错误信息:%s", "该用户不是本站点管理员,无法登录"),
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
user.Token, err = utils.GenerateRandomString(32)
|
||||
if err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusBadRequest,
|
||||
Title: "Something wrong",
|
||||
Msg: err.Error(),
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
user.TokenExpired = time.Now().AddDate(0, 2, 0)
|
||||
singleton.DB.Save(&user)
|
||||
c.SetCookie(singleton.Conf.Site.CookieName, user.Token, 60*60*24, "", "", false, false)
|
||||
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/redirect", mygin.CommonEnvironment(c, gin.H{
|
||||
"URL": "/",
|
||||
}))
|
||||
}
|
||||
|
||||
func removeDuplicates(elements []string) []string {
|
||||
encountered := map[string]bool{}
|
||||
result := []string{}
|
||||
|
||||
for _, v := range elements {
|
||||
if !encountered[v] {
|
||||
encountered[v] = true
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
166
cmd/dashboard/controller/server.go
Normal file
166
cmd/dashboard/controller/server.go
Normal file
@ -0,0 +1,166 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jinzhu/copier"
|
||||
|
||||
"github.com/nezhahq/nezha/model"
|
||||
"github.com/nezhahq/nezha/pkg/utils"
|
||||
pb "github.com/nezhahq/nezha/proto"
|
||||
"github.com/nezhahq/nezha/service/singleton"
|
||||
)
|
||||
|
||||
// List server
|
||||
// @Summary List server
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description List server
|
||||
// @Tags auth required
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[[]model.Server]
|
||||
// @Router /server [get]
|
||||
func listServer(c *gin.Context) ([]*model.Server, error) {
|
||||
singleton.SortedServerLock.RLock()
|
||||
defer singleton.SortedServerLock.RUnlock()
|
||||
|
||||
var ssl []*model.Server
|
||||
if err := copier.Copy(&ssl, &singleton.SortedServerList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ssl, nil
|
||||
}
|
||||
|
||||
// Edit server
|
||||
// @Summary Edit server
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description Edit server
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @Param id path uint true "Server ID"
|
||||
// @Param body body model.ServerForm true "ServerForm"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[any]
|
||||
// @Router /server/{id} [patch]
|
||||
func updateServer(c *gin.Context) (any, error) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var sf model.ServerForm
|
||||
if err := c.ShouldBindJSON(&sf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var s model.Server
|
||||
if err := singleton.DB.First(&s, id).Error; err != nil {
|
||||
return nil, singleton.Localizer.ErrorT("server id %d does not exist", id)
|
||||
}
|
||||
|
||||
s.Name = sf.Name
|
||||
s.DisplayIndex = sf.DisplayIndex
|
||||
s.Note = sf.Note
|
||||
s.PublicNote = sf.PublicNote
|
||||
s.HideForGuest = sf.HideForGuest
|
||||
s.EnableDDNS = sf.EnableDDNS
|
||||
s.DDNSProfiles = sf.DDNSProfiles
|
||||
ddnsProfilesRaw, err := utils.Json.Marshal(s.DDNSProfiles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.DDNSProfilesRaw = string(ddnsProfilesRaw)
|
||||
|
||||
if err := singleton.DB.Save(&s).Error; err != nil {
|
||||
return nil, newGormError("%v", err)
|
||||
}
|
||||
|
||||
singleton.ServerLock.Lock()
|
||||
s.CopyFromRunningServer(singleton.ServerList[s.ID])
|
||||
singleton.ServerList[s.ID] = &s
|
||||
singleton.ServerLock.Unlock()
|
||||
singleton.ReSortServer()
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Batch delete server
|
||||
// @Summary Batch delete server
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description Batch delete server
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @param request body []uint64 true "id list"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[any]
|
||||
// @Router /batch-delete/server [post]
|
||||
func batchDeleteServer(c *gin.Context) (any, error) {
|
||||
var servers []uint64
|
||||
if err := c.ShouldBindJSON(&servers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := singleton.DB.Unscoped().Delete(&model.Server{}, "id in (?)", servers).Error; err != nil {
|
||||
return nil, newGormError("%v", err)
|
||||
}
|
||||
|
||||
singleton.AlertsLock.Lock()
|
||||
for _, sid := range servers {
|
||||
for _, alert := range singleton.Alerts {
|
||||
if singleton.AlertsCycleTransferStatsStore[alert.ID] != nil {
|
||||
delete(singleton.AlertsCycleTransferStatsStore[alert.ID].ServerName, sid)
|
||||
delete(singleton.AlertsCycleTransferStatsStore[alert.ID].Transfer, sid)
|
||||
delete(singleton.AlertsCycleTransferStatsStore[alert.ID].NextUpdate, sid)
|
||||
}
|
||||
}
|
||||
}
|
||||
singleton.DB.Unscoped().Delete(&model.Transfer{}, "server_id in (?)", servers)
|
||||
singleton.AlertsLock.Unlock()
|
||||
|
||||
singleton.OnServerDelete(servers)
|
||||
singleton.ReSortServer()
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Force update Agent
|
||||
// @Summary Force update Agent
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description Force update Agent
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @param request body []uint64 true "id list"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[model.ForceUpdateResponse]
|
||||
// @Router /force-update/server [post]
|
||||
func forceUpdateServer(c *gin.Context) (*model.ForceUpdateResponse, error) {
|
||||
var forceUpdateServers []uint64
|
||||
if err := c.ShouldBindJSON(&forceUpdateServers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
forceUpdateResp := new(model.ForceUpdateResponse)
|
||||
|
||||
for _, sid := range forceUpdateServers {
|
||||
singleton.ServerLock.RLock()
|
||||
server := singleton.ServerList[sid]
|
||||
singleton.ServerLock.RUnlock()
|
||||
if server != nil && server.TaskStream != nil {
|
||||
if err := server.TaskStream.Send(&pb.Task{
|
||||
Type: model.TaskTypeUpgrade,
|
||||
}); err != nil {
|
||||
forceUpdateResp.Failure = append(forceUpdateResp.Failure, sid)
|
||||
} else {
|
||||
forceUpdateResp.Success = append(forceUpdateResp.Success, sid)
|
||||
}
|
||||
} else {
|
||||
forceUpdateResp.Offline = append(forceUpdateResp.Offline, sid)
|
||||
}
|
||||
}
|
||||
|
||||
return forceUpdateResp, nil
|
||||
}
|
199
cmd/dashboard/controller/server_group.go
Normal file
199
cmd/dashboard/controller/server_group.go
Normal file
@ -0,0 +1,199 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/nezhahq/nezha/model"
|
||||
"github.com/nezhahq/nezha/service/singleton"
|
||||
)
|
||||
|
||||
// List server group
|
||||
// @Summary List server group
|
||||
// @Schemes
|
||||
// @Description List server group
|
||||
// @Security BearerAuth
|
||||
// @Tags common
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[[]model.ServerGroupResponseItem]
|
||||
// @Router /server-group [get]
|
||||
func listServerGroup(c *gin.Context) ([]model.ServerGroupResponseItem, error) {
|
||||
var sg []model.ServerGroup
|
||||
if err := singleton.DB.Find(&sg).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
groupServers := make(map[uint64][]uint64, 0)
|
||||
var sgs []model.ServerGroupServer
|
||||
if err := singleton.DB.Find(&sgs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, s := range sgs {
|
||||
if _, ok := groupServers[s.ServerGroupId]; !ok {
|
||||
groupServers[s.ServerGroupId] = make([]uint64, 0)
|
||||
}
|
||||
groupServers[s.ServerGroupId] = append(groupServers[s.ServerGroupId], s.ServerId)
|
||||
}
|
||||
|
||||
var sgRes []model.ServerGroupResponseItem
|
||||
for _, s := range sg {
|
||||
sgRes = append(sgRes, model.ServerGroupResponseItem{
|
||||
Group: s,
|
||||
Servers: groupServers[s.ID],
|
||||
})
|
||||
}
|
||||
|
||||
return sgRes, nil
|
||||
}
|
||||
|
||||
// New server group
|
||||
// @Summary New server group
|
||||
// @Schemes
|
||||
// @Description New server group
|
||||
// @Security BearerAuth
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @Param body body model.ServerGroupForm true "ServerGroupForm"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[uint64]
|
||||
// @Router /server-group [post]
|
||||
func createServerGroup(c *gin.Context) (uint64, error) {
|
||||
var sgf model.ServerGroupForm
|
||||
if err := c.ShouldBindJSON(&sgf); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
sgf.Servers = slices.Compact(sgf.Servers)
|
||||
|
||||
var sg model.ServerGroup
|
||||
sg.Name = sgf.Name
|
||||
|
||||
var count int64
|
||||
if err := singleton.DB.Model(&model.Server{}).Where("id in (?)", sgf.Servers).Count(&count).Error; err != nil {
|
||||
return 0, newGormError("%v", err)
|
||||
}
|
||||
if count != int64(len(sgf.Servers)) {
|
||||
return 0, singleton.Localizer.ErrorT("have invalid server id")
|
||||
}
|
||||
|
||||
err := singleton.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Create(&sg).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, s := range sgf.Servers {
|
||||
if err := tx.Create(&model.ServerGroupServer{
|
||||
ServerGroupId: sg.ID,
|
||||
ServerId: s,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return 0, newGormError("%v", err)
|
||||
}
|
||||
|
||||
return sg.ID, nil
|
||||
}
|
||||
|
||||
// Edit server group
|
||||
// @Summary Edit server group
|
||||
// @Schemes
|
||||
// @Description Edit server group
|
||||
// @Security BearerAuth
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @Param id path uint true "ID"
|
||||
// @Param body body model.ServerGroupForm true "ServerGroupForm"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[any]
|
||||
// @Router /server-group/{id} [patch]
|
||||
func updateServerGroup(c *gin.Context) (any, error) {
|
||||
idStr := c.Param("id")
|
||||
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var sg model.ServerGroupForm
|
||||
if err := c.ShouldBindJSON(&sg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sg.Servers = slices.Compact(sg.Servers)
|
||||
|
||||
var sgDB model.ServerGroup
|
||||
if err := singleton.DB.First(&sgDB, id).Error; err != nil {
|
||||
return nil, singleton.Localizer.ErrorT("group id %d does not exist", id)
|
||||
}
|
||||
sgDB.Name = sg.Name
|
||||
|
||||
var count int64
|
||||
if err := singleton.DB.Model(&model.Server{}).Where("id in (?)", sg.Servers).Count(&count).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if count != int64(len(sg.Servers)) {
|
||||
return nil, singleton.Localizer.ErrorT("have invalid server id")
|
||||
}
|
||||
|
||||
err = singleton.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Save(&sgDB).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Unscoped().Delete(&model.ServerGroupServer{}, "server_group_id = ?", id).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, s := range sg.Servers {
|
||||
if err := tx.Create(&model.ServerGroupServer{
|
||||
ServerGroupId: sgDB.ID,
|
||||
ServerId: s,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, newGormError("%v", err)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Batch delete server group
|
||||
// @Summary Batch delete server group
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description Batch delete server group
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @param request body []uint64 true "id list"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[any]
|
||||
// @Router /batch-delete/server-group [post]
|
||||
func batchDeleteServerGroup(c *gin.Context) (any, error) {
|
||||
var sgs []uint64
|
||||
if err := c.ShouldBindJSON(&sgs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err := singleton.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Unscoped().Delete(&model.ServerGroup{}, "id in (?)", sgs).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Unscoped().Delete(&model.ServerGroupServer{}, "server_group_id in (?)", sgs).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, newGormError("%v", err)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
313
cmd/dashboard/controller/service.go
Normal file
313
cmd/dashboard/controller/service.go
Normal file
@ -0,0 +1,313 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jinzhu/copier"
|
||||
|
||||
"github.com/nezhahq/nezha/model"
|
||||
"github.com/nezhahq/nezha/service/singleton"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// List service
|
||||
// @Summary List service
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description List service
|
||||
// @Tags common
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[model.ServiceResponse]
|
||||
// @Router /service [get]
|
||||
func listService(c *gin.Context) (*model.ServiceResponse, error) {
|
||||
res, err, _ := requestGroup.Do("list-service", func() (interface{}, error) {
|
||||
singleton.AlertsLock.RLock()
|
||||
defer singleton.AlertsLock.RUnlock()
|
||||
var stats map[uint64]model.ServiceResponseItem
|
||||
var statsStore map[uint64]model.CycleTransferStats
|
||||
copier.Copy(&stats, singleton.ServiceSentinelShared.LoadStats())
|
||||
copier.Copy(&statsStore, singleton.AlertsCycleTransferStatsStore)
|
||||
_, isMember := c.Get(model.CtxKeyAuthorizedUser)
|
||||
authorized := isMember // TODO || isViewPasswordVerfied
|
||||
for k, service := range stats {
|
||||
if !authorized {
|
||||
if !service.Service.EnableShowInService {
|
||||
delete(stats, k)
|
||||
}
|
||||
service.Service = &model.Service{Name: service.Service.Name}
|
||||
stats[k] = service
|
||||
}
|
||||
}
|
||||
return []interface {
|
||||
}{
|
||||
stats, statsStore,
|
||||
}, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.ServiceResponse{
|
||||
Services: res.([]interface{})[0].(map[uint64]model.ServiceResponseItem),
|
||||
CycleTransferStats: res.([]interface{})[1].(map[uint64]model.CycleTransferStats),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// List service histories by server id
|
||||
// @Summary List service histories by server id
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description List service histories by server id
|
||||
// @Tags common
|
||||
// @param id path uint true "Server ID"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[[]model.ServiceInfos]
|
||||
// @Router /service/{id} [get]
|
||||
func listServiceHistory(c *gin.Context) ([]*model.ServiceInfos, error) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
singleton.ServerLock.RLock()
|
||||
server, ok := singleton.ServerList[id]
|
||||
if !ok {
|
||||
return nil, singleton.Localizer.ErrorT("server not found")
|
||||
}
|
||||
|
||||
_, isMember := c.Get(model.CtxKeyAuthorizedUser)
|
||||
authorized := isMember // TODO || isViewPasswordVerfied
|
||||
|
||||
if server.HideForGuest && !authorized {
|
||||
return nil, singleton.Localizer.ErrorT("unauthorized")
|
||||
}
|
||||
singleton.ServerLock.RUnlock()
|
||||
|
||||
var serviceHistories []*model.ServiceHistory
|
||||
if err := singleton.DB.Model(&model.ServiceHistory{}).Select("service_id, created_at, server_id, avg_delay").
|
||||
Where("server_id = ?", id).Where("created_at >= ?", time.Now().Add(-24*time.Hour)).Order("service_id, created_at").
|
||||
Scan(&serviceHistories).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
singleton.ServiceSentinelShared.ServicesLock.RLock()
|
||||
defer singleton.ServiceSentinelShared.ServicesLock.RUnlock()
|
||||
singleton.ServerLock.RLock()
|
||||
defer singleton.ServerLock.RUnlock()
|
||||
|
||||
var sortedServiceIDs []uint64
|
||||
resultMap := make(map[uint64]*model.ServiceInfos)
|
||||
for _, history := range serviceHistories {
|
||||
infos, ok := resultMap[history.ServiceID]
|
||||
if !ok {
|
||||
infos = &model.ServiceInfos{
|
||||
ServiceID: history.ServiceID,
|
||||
ServerID: history.ServerID,
|
||||
ServiceName: singleton.ServiceSentinelShared.Services[history.ServiceID].Name,
|
||||
ServerName: singleton.ServerList[history.ServerID].Name,
|
||||
}
|
||||
resultMap[history.ServiceID] = infos
|
||||
sortedServiceIDs = append(sortedServiceIDs, history.ServiceID)
|
||||
}
|
||||
infos.CreatedAt = append(infos.CreatedAt, history.CreatedAt.Truncate(time.Minute).Unix()*1000)
|
||||
infos.AvgDelay = append(infos.AvgDelay, history.AvgDelay)
|
||||
}
|
||||
|
||||
ret := make([]*model.ServiceInfos, 0, len(sortedServiceIDs))
|
||||
for _, id := range sortedServiceIDs {
|
||||
ret = append(ret, resultMap[id])
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// List server with service
|
||||
// @Summary List server with service
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description List server with service
|
||||
// @Tags common
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[[]uint64]
|
||||
// @Router /service/server [get]
|
||||
func listServerWithServices(c *gin.Context) ([]uint64, error) {
|
||||
var serverIdsWithService []uint64
|
||||
if err := singleton.DB.Model(&model.ServiceHistory{}).
|
||||
Select("distinct(server_id)").
|
||||
Where("server_id != 0").
|
||||
Find(&serverIdsWithService).Error; err != nil {
|
||||
return nil, newGormError("%v", err)
|
||||
}
|
||||
|
||||
_, isMember := c.Get(model.CtxKeyAuthorizedUser)
|
||||
authorized := isMember // TODO || isViewPasswordVerfied
|
||||
|
||||
var ret []uint64
|
||||
for _, id := range serverIdsWithService {
|
||||
singleton.ServerLock.RLock()
|
||||
server, ok := singleton.ServerList[id]
|
||||
if !ok {
|
||||
singleton.ServerLock.RUnlock()
|
||||
return nil, singleton.Localizer.ErrorT("server not found")
|
||||
}
|
||||
|
||||
if !server.HideForGuest || authorized {
|
||||
ret = append(ret, id)
|
||||
}
|
||||
singleton.ServerLock.RUnlock()
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Create service
|
||||
// @Summary Create service
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description Create service
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @param request body model.ServiceForm true "Service Request"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[uint64]
|
||||
// @Router /service [post]
|
||||
func createService(c *gin.Context) (uint64, error) {
|
||||
var mf model.ServiceForm
|
||||
if err := c.ShouldBindJSON(&mf); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var m model.Service
|
||||
m.Name = mf.Name
|
||||
m.Target = strings.TrimSpace(mf.Target)
|
||||
m.Type = mf.Type
|
||||
m.SkipServers = mf.SkipServers
|
||||
m.Cover = mf.Cover
|
||||
m.Notify = mf.Notify
|
||||
m.NotificationGroupID = mf.NotificationGroupID
|
||||
m.Duration = mf.Duration
|
||||
m.LatencyNotify = mf.LatencyNotify
|
||||
m.MinLatency = mf.MinLatency
|
||||
m.MaxLatency = mf.MaxLatency
|
||||
m.EnableShowInService = mf.EnableShowInService
|
||||
m.EnableTriggerTask = mf.EnableTriggerTask
|
||||
m.RecoverTriggerTasks = mf.RecoverTriggerTasks
|
||||
m.FailTriggerTasks = mf.FailTriggerTasks
|
||||
|
||||
if err := singleton.DB.Create(&m).Error; err != nil {
|
||||
return 0, newGormError("%v", err)
|
||||
}
|
||||
|
||||
var skipServers []uint64
|
||||
for k := range m.SkipServers {
|
||||
skipServers = append(skipServers, k)
|
||||
}
|
||||
|
||||
var err error
|
||||
if m.Cover == 0 {
|
||||
err = singleton.DB.Unscoped().Delete(&model.ServiceHistory{}, "service_id = ? and server_id in (?)", m.ID, skipServers).Error
|
||||
} else {
|
||||
err = singleton.DB.Unscoped().Delete(&model.ServiceHistory{}, "service_id = ? and server_id not in (?)", m.ID, skipServers).Error
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return m.ID, singleton.ServiceSentinelShared.OnServiceUpdate(m)
|
||||
}
|
||||
|
||||
// Update service
|
||||
// @Summary Update service
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description Update service
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @param id path uint true "Service ID"
|
||||
// @param request body model.ServiceForm true "Service Request"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[any]
|
||||
// @Router /service/{id} [patch]
|
||||
func updateService(c *gin.Context) (any, error) {
|
||||
strID := c.Param("id")
|
||||
id, err := strconv.ParseUint(strID, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var mf model.ServiceForm
|
||||
if err := c.ShouldBindJSON(&mf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var m model.Service
|
||||
if err := singleton.DB.First(&m, id).Error; err != nil {
|
||||
return nil, singleton.Localizer.ErrorT("service id %d does not exist", id)
|
||||
}
|
||||
m.Name = mf.Name
|
||||
m.Target = strings.TrimSpace(mf.Target)
|
||||
m.Type = mf.Type
|
||||
m.SkipServers = mf.SkipServers
|
||||
m.Cover = mf.Cover
|
||||
m.Notify = mf.Notify
|
||||
m.NotificationGroupID = mf.NotificationGroupID
|
||||
m.Duration = mf.Duration
|
||||
m.LatencyNotify = mf.LatencyNotify
|
||||
m.MinLatency = mf.MinLatency
|
||||
m.MaxLatency = mf.MaxLatency
|
||||
m.EnableShowInService = mf.EnableShowInService
|
||||
m.EnableTriggerTask = mf.EnableTriggerTask
|
||||
m.RecoverTriggerTasks = mf.RecoverTriggerTasks
|
||||
m.FailTriggerTasks = mf.FailTriggerTasks
|
||||
|
||||
if err := singleton.DB.Save(&m).Error; err != nil {
|
||||
return nil, newGormError("%v", err)
|
||||
}
|
||||
|
||||
var skipServers []uint64
|
||||
for k := range m.SkipServers {
|
||||
skipServers = append(skipServers, k)
|
||||
}
|
||||
|
||||
if m.Cover == 0 {
|
||||
err = singleton.DB.Unscoped().Delete(&model.ServiceHistory{}, "service_id = ? and server_id in (?)", m.ID, skipServers).Error
|
||||
} else {
|
||||
err = singleton.DB.Unscoped().Delete(&model.ServiceHistory{}, "service_id = ? and server_id not in (?)", m.ID, skipServers).Error
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, singleton.ServiceSentinelShared.OnServiceUpdate(m)
|
||||
}
|
||||
|
||||
// Batch delete service
|
||||
// @Summary Batch delete service
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description Batch delete service
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @param request body []uint true "id list"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[any]
|
||||
// @Router /batch-delete/service [post]
|
||||
func batchDeleteService(c *gin.Context) (any, error) {
|
||||
var ids []uint64
|
||||
if err := c.ShouldBindJSON(&ids); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err := singleton.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Unscoped().Delete(&model.Service{}, "id in (?)", ids).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Unscoped().Delete(&model.ServiceHistory{}, "service_id in (?)", ids).Error
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
singleton.ServiceSentinelShared.OnServiceDelete(ids)
|
||||
return nil, nil
|
||||
}
|
73
cmd/dashboard/controller/setting.go
Normal file
73
cmd/dashboard/controller/setting.go
Normal file
@ -0,0 +1,73 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/nezhahq/nezha/model"
|
||||
"github.com/nezhahq/nezha/service/singleton"
|
||||
)
|
||||
|
||||
// List settings
|
||||
// @Summary List settings
|
||||
// @Schemes
|
||||
// @Description List settings
|
||||
// @Security BearerAuth
|
||||
// @Tags common
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[model.Config]
|
||||
// @Router /setting [get]
|
||||
func listConfig(c *gin.Context) (model.Config, error) {
|
||||
_, isMember := c.Get(model.CtxKeyAuthorizedUser)
|
||||
authorized := isMember // TODO || isViewPasswordVerfied
|
||||
|
||||
conf := *singleton.Conf
|
||||
if !authorized {
|
||||
conf = model.Config{
|
||||
SiteName: conf.SiteName,
|
||||
Language: conf.Language,
|
||||
CustomCode: conf.CustomCode,
|
||||
CustomCodeDashboard: conf.CustomCodeDashboard,
|
||||
}
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
// Edit config
|
||||
// @Summary Edit config
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description Edit config
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @Param body body model.SettingForm true "SettingForm"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[any]
|
||||
// @Router /setting [patch]
|
||||
func updateConfig(c *gin.Context) (any, error) {
|
||||
var sf model.SettingForm
|
||||
if err := c.ShouldBindJSON(&sf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
singleton.Conf.Language = sf.Language
|
||||
singleton.Conf.EnableIPChangeNotification = sf.EnableIPChangeNotification
|
||||
singleton.Conf.EnablePlainIPInNotification = sf.EnablePlainIPInNotification
|
||||
singleton.Conf.Cover = sf.Cover
|
||||
singleton.Conf.InstallHost = sf.InstallHost
|
||||
singleton.Conf.IgnoredIPNotification = sf.IgnoredIPNotification
|
||||
singleton.Conf.IPChangeNotificationGroupID = sf.IPChangeNotificationGroupID
|
||||
singleton.Conf.SiteName = sf.SiteName
|
||||
singleton.Conf.DNSServers = sf.CustomNameservers
|
||||
singleton.Conf.CustomCode = sf.CustomCode
|
||||
singleton.Conf.CustomCodeDashboard = sf.CustomCodeDashboard
|
||||
singleton.Conf.RealIPHeader = sf.RealIPHeader
|
||||
|
||||
if err := singleton.Conf.Save(); err != nil {
|
||||
return nil, newGormError("%v", err)
|
||||
}
|
||||
|
||||
singleton.OnNameserverUpdate()
|
||||
singleton.OnUpdateLang(singleton.Conf.Language)
|
||||
return nil, nil
|
||||
}
|
103
cmd/dashboard/controller/terminal.go
Normal file
103
cmd/dashboard/controller/terminal.go
Normal file
@ -0,0 +1,103 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/hashicorp/go-uuid"
|
||||
"github.com/nezhahq/nezha/model"
|
||||
"github.com/nezhahq/nezha/pkg/utils"
|
||||
"github.com/nezhahq/nezha/pkg/websocketx"
|
||||
"github.com/nezhahq/nezha/proto"
|
||||
"github.com/nezhahq/nezha/service/rpc"
|
||||
"github.com/nezhahq/nezha/service/singleton"
|
||||
)
|
||||
|
||||
// Create web ssh terminal
|
||||
// @Summary Create web ssh terminal
|
||||
// @Description Create web ssh terminal
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @Param terminal body model.TerminalForm true "TerminalForm"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CreateTerminalResponse
|
||||
// @Router /terminal [post]
|
||||
func createTerminal(c *gin.Context) (*model.CreateTerminalResponse, error) {
|
||||
var createTerminalReq model.TerminalForm
|
||||
if err := c.ShouldBind(&createTerminalReq); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
streamId, err := uuid.GenerateUUID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rpc.NezhaHandlerSingleton.CreateStream(streamId)
|
||||
|
||||
singleton.ServerLock.RLock()
|
||||
server := singleton.ServerList[createTerminalReq.ServerID]
|
||||
singleton.ServerLock.RUnlock()
|
||||
if server == nil || server.TaskStream == nil {
|
||||
return nil, singleton.Localizer.ErrorT("server not found or not connected")
|
||||
}
|
||||
|
||||
terminalData, _ := utils.Json.Marshal(&model.TerminalTask{
|
||||
StreamID: streamId,
|
||||
})
|
||||
if err := server.TaskStream.Send(&proto.Task{
|
||||
Type: model.TaskTypeTerminalGRPC,
|
||||
Data: string(terminalData),
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.CreateTerminalResponse{
|
||||
SessionID: streamId,
|
||||
ServerID: server.ID,
|
||||
ServerName: server.Name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TerminalStream web ssh terminal stream
|
||||
// @Summary Terminal stream
|
||||
// @Description Terminal stream
|
||||
// @Tags auth required
|
||||
// @Param id path string true "Stream UUID"
|
||||
// @Success 200 {object} model.CommonResponse[any]
|
||||
// @Router /ws/terminal/{id} [get]
|
||||
func terminalStream(c *gin.Context) (any, error) {
|
||||
streamId := c.Param("id")
|
||||
if _, err := rpc.NezhaHandlerSingleton.GetStream(streamId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rpc.NezhaHandlerSingleton.CloseStream(streamId)
|
||||
|
||||
wsConn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
return nil, newWsError("%v", err)
|
||||
}
|
||||
defer wsConn.Close()
|
||||
conn := websocketx.NewConn(wsConn)
|
||||
|
||||
go func() {
|
||||
// PING 保活
|
||||
for {
|
||||
if err = conn.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
|
||||
return
|
||||
}
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}()
|
||||
|
||||
if err = rpc.NezhaHandlerSingleton.UserConnected(streamId, conn); err != nil {
|
||||
return nil, newWsError("%v", err)
|
||||
}
|
||||
|
||||
if err = rpc.NezhaHandlerSingleton.StartStream(streamId, time.Second*10); err != nil {
|
||||
return nil, newWsError("%v", err)
|
||||
}
|
||||
|
||||
return nil, newWsError("")
|
||||
}
|
153
cmd/dashboard/controller/user.go
Normal file
153
cmd/dashboard/controller/user.go
Normal file
@ -0,0 +1,153 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/nezhahq/nezha/model"
|
||||
"github.com/nezhahq/nezha/service/singleton"
|
||||
)
|
||||
|
||||
// Get profile
|
||||
// @Summary Get profile
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description Get profile
|
||||
// @Tags auth required
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[model.Profile]
|
||||
// @Router /profile [get]
|
||||
func getProfile(c *gin.Context) (*model.Profile, error) {
|
||||
auth, ok := c.Get(model.CtxKeyAuthorizedUser)
|
||||
if !ok {
|
||||
return nil, singleton.Localizer.ErrorT("unauthorized")
|
||||
}
|
||||
return &model.Profile{
|
||||
User: *auth.(*model.User),
|
||||
LoginIP: c.GetString(model.CtxKeyRealIPStr),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Update password for current user
|
||||
// @Summary Update password for current user
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description Update password for current user
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @param request body model.ProfileForm true "password"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[any]
|
||||
// @Router /profile [post]
|
||||
func updateProfile(c *gin.Context) (any, error) {
|
||||
var pf model.ProfileForm
|
||||
if err := c.ShouldBindJSON(&pf); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
auth, ok := c.Get(model.CtxKeyAuthorizedUser)
|
||||
if !ok {
|
||||
return nil, singleton.Localizer.ErrorT("unauthorized")
|
||||
}
|
||||
|
||||
user := *auth.(*model.User)
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(pf.OriginalPassword)); err != nil {
|
||||
return nil, singleton.Localizer.ErrorT("incorrect password")
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(pf.NewPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user.Username = pf.NewUsername
|
||||
user.Password = string(hash)
|
||||
if err := singleton.DB.Save(&user).Error; err != nil {
|
||||
return nil, newGormError("%v", err)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// List user
|
||||
// @Summary List user
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description List user
|
||||
// @Tags auth required
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[[]model.User]
|
||||
// @Router /user [get]
|
||||
func listUser(c *gin.Context) ([]model.User, error) {
|
||||
var users []model.User
|
||||
if err := singleton.DB.Find(&users).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// Create user
|
||||
// @Summary Create user
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description Create user
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @param request body model.UserForm true "User Request"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[uint64]
|
||||
// @Router /user [post]
|
||||
func createUser(c *gin.Context) (uint64, error) {
|
||||
var uf model.UserForm
|
||||
if err := c.ShouldBindJSON(&uf); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if len(uf.Password) < 6 {
|
||||
return 0, singleton.Localizer.ErrorT("password length must be greater than 6")
|
||||
}
|
||||
if uf.Username == "" {
|
||||
return 0, singleton.Localizer.ErrorT("username can't be empty")
|
||||
}
|
||||
|
||||
var u model.User
|
||||
u.Username = uf.Username
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(uf.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
u.Password = string(hash)
|
||||
|
||||
if err := singleton.DB.Create(&u).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return u.ID, nil
|
||||
}
|
||||
|
||||
// Batch delete users
|
||||
// @Summary Batch delete users
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description Batch delete users
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @param request body []uint true "id list"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[any]
|
||||
// @Router /batch-delete/user [post]
|
||||
func batchDeleteUser(c *gin.Context) (any, error) {
|
||||
var ids []uint64
|
||||
if err := c.ShouldBindJSON(&ids); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
auth := c.MustGet(model.CtxKeyAuthorizedUser).(*model.User)
|
||||
if slices.Contains(ids, auth.ID) {
|
||||
return nil, singleton.Localizer.ErrorT("can't delete yourself")
|
||||
}
|
||||
|
||||
return nil, singleton.DB.Where("id IN (?)", ids).Delete(&model.User{}).Error
|
||||
}
|
50
cmd/dashboard/controller/waf.go
Normal file
50
cmd/dashboard/controller/waf.go
Normal file
@ -0,0 +1,50 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/nezhahq/nezha/model"
|
||||
"github.com/nezhahq/nezha/service/singleton"
|
||||
)
|
||||
|
||||
// List blocked addresses
|
||||
// @Summary List blocked addresses
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description List server
|
||||
// @Tags auth required
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[[]model.WAFApiMock]
|
||||
// @Router /waf [get]
|
||||
func listBlockedAddress(c *gin.Context) ([]*model.WAF, error) {
|
||||
var waf []*model.WAF
|
||||
if err := singleton.DB.Find(&waf).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return waf, nil
|
||||
}
|
||||
|
||||
// Batch delete blocked addresses
|
||||
// @Summary Edit server
|
||||
// @Security BearerAuth
|
||||
// @Schemes
|
||||
// @Description Edit server
|
||||
// @Tags auth required
|
||||
// @Accept json
|
||||
// @Param request body []string true "block list"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.CommonResponse[any]
|
||||
// @Router /batch-delete/waf [patch]
|
||||
func batchDeleteBlockedAddress(c *gin.Context) (any, error) {
|
||||
var list []string
|
||||
if err := c.ShouldBindJSON(&list); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := model.BatchClearIP(singleton.DB, list); err != nil {
|
||||
return nil, newGormError("%v", err)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
57
cmd/dashboard/controller/waf/waf.go
Normal file
57
cmd/dashboard/controller/waf/waf.go
Normal file
@ -0,0 +1,57 @@
|
||||
package waf
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/nezhahq/nezha/model"
|
||||
"github.com/nezhahq/nezha/pkg/utils"
|
||||
"github.com/nezhahq/nezha/service/singleton"
|
||||
)
|
||||
|
||||
//go:embed waf.html
|
||||
var errorPageTemplate string
|
||||
|
||||
func RealIp(c *gin.Context) {
|
||||
if singleton.Conf.RealIPHeader == "" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
if singleton.Conf.RealIPHeader == model.ConfigUsePeerIP {
|
||||
c.Set(model.CtxKeyRealIPStr, c.RemoteIP())
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
vals := c.Request.Header.Get(singleton.Conf.RealIPHeader)
|
||||
if vals == "" {
|
||||
c.AbortWithStatusJSON(http.StatusOK, model.CommonResponse[any]{Success: false, Error: "real ip header not found"})
|
||||
return
|
||||
}
|
||||
ip, err := utils.GetIPFromHeader(vals)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusOK, model.CommonResponse[any]{Success: false, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
c.Set(model.CtxKeyRealIPStr, ip)
|
||||
c.Next()
|
||||
}
|
||||
|
||||
func Waf(c *gin.Context) {
|
||||
if err := model.CheckIP(singleton.DB, c.GetString(model.CtxKeyRealIPStr)); err != nil {
|
||||
ShowBlockPage(c, err)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
|
||||
func ShowBlockPage(c *gin.Context, err error) {
|
||||
c.Writer.WriteHeader(http.StatusForbidden)
|
||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||
c.Writer.WriteString(strings.Replace(errorPageTemplate, "{error}", err.Error(), 1))
|
||||
c.Abort()
|
||||
}
|
46
cmd/dashboard/controller/waf/waf.html
Normal file
46
cmd/dashboard/controller/waf/waf.html
Normal file
@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Blocked</title>
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 90vh;
|
||||
font-weight: bolder;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
}
|
||||
main {
|
||||
text-align: center;
|
||||
}
|
||||
.emoji {
|
||||
font-size: 200px;
|
||||
}
|
||||
p.secondary {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #111;
|
||||
color: #007C41
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main>
|
||||
<div class="emoji">🤡</div>
|
||||
<h1>Blocked</h1>
|
||||
<p>{error}</p>
|
||||
<p class="secondary">nezha WAF</p>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
134
cmd/dashboard/controller/ws.go
Normal file
134
cmd/dashboard/controller/ws.go
Normal file
@ -0,0 +1,134 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"golang.org/x/sync/singleflight"
|
||||
|
||||
"github.com/nezhahq/nezha/model"
|
||||
"github.com/nezhahq/nezha/pkg/utils"
|
||||
"github.com/nezhahq/nezha/service/singleton"
|
||||
)
|
||||
|
||||
var upgrader *websocket.Upgrader
|
||||
|
||||
func InitUpgrader() {
|
||||
var checkOrigin func(r *http.Request) bool
|
||||
|
||||
// Allow CORS from loopback addresses in debug mode
|
||||
if singleton.Conf.Debug {
|
||||
checkOrigin = func(r *http.Request) bool {
|
||||
hostAddr := r.Host
|
||||
host, _, err := net.SplitHostPort(hostAddr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
if ip.IsLoopback() {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
// Handle domains like "localhost"
|
||||
ip, err := net.LookupHost(host)
|
||||
if err != nil || len(ip) == 0 {
|
||||
return false
|
||||
}
|
||||
if netIP := net.ParseIP(ip[0]); netIP != nil && netIP.IsLoopback() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
upgrader = &websocket.Upgrader{
|
||||
ReadBufferSize: 32768,
|
||||
WriteBufferSize: 32768,
|
||||
CheckOrigin: checkOrigin,
|
||||
}
|
||||
}
|
||||
|
||||
// Websocket server stream
|
||||
// @Summary Websocket server stream
|
||||
// @tags common
|
||||
// @Schemes
|
||||
// @Description Websocket server stream
|
||||
// @security BearerAuth
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.StreamServerData
|
||||
// @Router /ws/server [get]
|
||||
func serverStream(c *gin.Context) (any, error) {
|
||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
return nil, newWsError("%v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
count := 0
|
||||
for {
|
||||
stat, err := getServerStat(c, count == 0)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if err := conn.WriteMessage(websocket.TextMessage, stat); err != nil {
|
||||
break
|
||||
}
|
||||
count += 1
|
||||
if count%4 == 0 {
|
||||
err = conn.WriteMessage(websocket.PingMessage, []byte{})
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
time.Sleep(time.Second * 2)
|
||||
}
|
||||
return nil, newWsError("")
|
||||
}
|
||||
|
||||
var requestGroup singleflight.Group
|
||||
|
||||
func getServerStat(c *gin.Context, withPublicNote bool) ([]byte, error) {
|
||||
_, isMember := c.Get(model.CtxKeyAuthorizedUser)
|
||||
authorized := isMember // TODO || isViewPasswordVerfied
|
||||
v, err, _ := requestGroup.Do(fmt.Sprintf("serverStats::%t", authorized), func() (interface{}, error) {
|
||||
singleton.SortedServerLock.RLock()
|
||||
defer singleton.SortedServerLock.RUnlock()
|
||||
|
||||
var serverList []*model.Server
|
||||
if authorized {
|
||||
serverList = singleton.SortedServerList
|
||||
} else {
|
||||
serverList = singleton.SortedServerListForGuest
|
||||
}
|
||||
|
||||
servers := make([]model.StreamServer, 0, len(serverList))
|
||||
for _, server := range serverList {
|
||||
var countryCode string
|
||||
if server.GeoIP != nil {
|
||||
countryCode = server.GeoIP.CountryCode
|
||||
}
|
||||
servers = append(servers, model.StreamServer{
|
||||
ID: server.ID,
|
||||
Name: server.Name,
|
||||
PublicNote: utils.IfOr(withPublicNote, server.PublicNote, ""),
|
||||
DisplayIndex: server.DisplayIndex,
|
||||
Host: server.Host,
|
||||
State: server.State,
|
||||
CountryCode: countryCode,
|
||||
LastActive: server.LastActive,
|
||||
})
|
||||
}
|
||||
|
||||
return utils.Json.Marshal(model.StreamServerData{
|
||||
Now: time.Now().Unix() * 1000,
|
||||
Servers: servers,
|
||||
})
|
||||
})
|
||||
|
||||
return v.([]byte), err
|
||||
}
|
@ -1,20 +1,29 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"context"
|
||||
"embed"
|
||||
_ "embed"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
_ "time/tzdata"
|
||||
|
||||
"github.com/naiba/nezha/cmd/dashboard/controller"
|
||||
"github.com/naiba/nezha/cmd/dashboard/rpc"
|
||||
"github.com/naiba/nezha/model"
|
||||
"github.com/naiba/nezha/proto"
|
||||
"github.com/naiba/nezha/service/singleton"
|
||||
"github.com/ory/graceful"
|
||||
flag "github.com/spf13/pflag"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c"
|
||||
|
||||
"github.com/nezhahq/nezha/cmd/dashboard/controller"
|
||||
"github.com/nezhahq/nezha/cmd/dashboard/rpc"
|
||||
"github.com/nezhahq/nezha/model"
|
||||
"github.com/nezhahq/nezha/proto"
|
||||
"github.com/nezhahq/nezha/service/singleton"
|
||||
)
|
||||
|
||||
type DashboardCliParam struct {
|
||||
@ -25,22 +34,37 @@ type DashboardCliParam struct {
|
||||
|
||||
var (
|
||||
dashboardCliParam DashboardCliParam
|
||||
//go:embed admin-dist
|
||||
adminFrontend embed.FS
|
||||
//go:embed user-dist
|
||||
userFrontend embed.FS
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.CommandLine.ParseErrorsWhitelist.UnknownFlags = true
|
||||
flag.BoolVarP(&dashboardCliParam.Version, "version", "v", false, "查看当前版本号")
|
||||
flag.StringVarP(&dashboardCliParam.ConfigFile, "config", "c", "data/config.yaml", "配置文件路径")
|
||||
flag.StringVar(&dashboardCliParam.DatebaseLocation, "db", "data/sqlite.db", "Sqlite3数据库文件路径")
|
||||
flag.Parse()
|
||||
}
|
||||
|
||||
func initSystem() {
|
||||
// 初始化管理员账户
|
||||
var usersCount int64
|
||||
if err := singleton.DB.Model(&model.User{}).Count(&usersCount).Error; err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if usersCount == 0 {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte("admin"), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
admin := model.User{
|
||||
Username: "admin",
|
||||
Password: string(hash),
|
||||
}
|
||||
if err := singleton.DB.Create(&admin).Error; err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// 启动 singleton 包下的所有服务
|
||||
singleton.LoadSingleton()
|
||||
|
||||
// 每天的3:30 对 监控记录 和 流量记录 进行清理
|
||||
if _, err := singleton.Cron.AddFunc("0 30 3 * * *", singleton.CleanMonitorHistory); err != nil {
|
||||
if _, err := singleton.Cron.AddFunc("0 30 3 * * *", singleton.CleanServiceHistory); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@ -50,7 +74,33 @@ func initSystem() {
|
||||
}
|
||||
}
|
||||
|
||||
// @title Nezha Monitoring API
|
||||
// @version 1.0
|
||||
// @description Nezha Monitoring API
|
||||
// @termsOfService http://nezhahq.github.io
|
||||
|
||||
// @contact.name API Support
|
||||
// @contact.url http://nezhahq.github.io
|
||||
// @contact.email hi@nai.ba
|
||||
|
||||
// @license.name Apache 2.0
|
||||
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
|
||||
// @host localhost:8008
|
||||
// @BasePath /api/v1
|
||||
|
||||
// @securityDefinitions.apikey BearerAuth
|
||||
// @in header
|
||||
// @name Authorization
|
||||
|
||||
// @externalDocs.description OpenAPI
|
||||
// @externalDocs.url https://swagger.io/resources/open-api/
|
||||
func main() {
|
||||
flag.BoolVar(&dashboardCliParam.Version, "v", false, "查看当前版本号")
|
||||
flag.StringVar(&dashboardCliParam.ConfigFile, "c", "data/config.yaml", "配置文件路径")
|
||||
flag.StringVar(&dashboardCliParam.DatebaseLocation, "db", "data/sqlite.db", "Sqlite3数据库文件路径")
|
||||
flag.Parse()
|
||||
|
||||
if dashboardCliParam.Version {
|
||||
fmt.Println(singleton.Version)
|
||||
os.Exit(0)
|
||||
@ -60,27 +110,38 @@ func main() {
|
||||
singleton.InitConfigFromPath(dashboardCliParam.ConfigFile)
|
||||
singleton.InitTimezoneAndCache()
|
||||
singleton.InitDBFromPath(dashboardCliParam.DatebaseLocation)
|
||||
singleton.InitLocalizer()
|
||||
initSystem()
|
||||
|
||||
// TODO 使用 cmux 在同一端口服务 HTTP 和 gRPC
|
||||
singleton.CleanMonitorHistory()
|
||||
go rpc.ServeRPC(singleton.Conf.GRPCPort)
|
||||
serviceSentinelDispatchBus := make(chan model.Monitor) // 用于传递服务监控任务信息的channel
|
||||
l, err := net.Listen("tcp", fmt.Sprintf(":%d", singleton.Conf.ListenPort))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
singleton.CleanServiceHistory()
|
||||
serviceSentinelDispatchBus := make(chan model.Service) // 用于传递服务监控任务信息的channel
|
||||
go rpc.DispatchTask(serviceSentinelDispatchBus)
|
||||
go rpc.DispatchKeepalive()
|
||||
go singleton.AlertSentinelStart()
|
||||
singleton.NewServiceSentinel(serviceSentinelDispatchBus)
|
||||
srv := controller.ServeWeb(singleton.Conf.HTTPPort)
|
||||
|
||||
grpcHandler := rpc.ServeRPC()
|
||||
httpHandler := controller.ServeWeb(adminFrontend, userFrontend)
|
||||
controller.InitUpgrader()
|
||||
|
||||
muxHandler := newHTTPandGRPCMux(httpHandler, grpcHandler)
|
||||
http2Server := &http2.Server{}
|
||||
muxServer := &http.Server{Handler: h2c.NewHandler(muxHandler, http2Server), ReadHeaderTimeout: time.Second * 5}
|
||||
|
||||
go dispatchReportInfoTask()
|
||||
|
||||
if err := graceful.Graceful(func() error {
|
||||
return srv.ListenAndServe()
|
||||
log.Println("NEZHA>> Dashboard::START", singleton.Conf.ListenPort)
|
||||
return muxServer.Serve(l)
|
||||
}, func(c context.Context) error {
|
||||
log.Println("NEZHA>> Graceful::START")
|
||||
singleton.RecordTransferHourlyUsage()
|
||||
log.Println("NEZHA>> Graceful::END")
|
||||
srv.Shutdown(c)
|
||||
return nil
|
||||
return muxServer.Shutdown(c)
|
||||
}); err != nil {
|
||||
log.Printf("NEZHA>> ERROR: %v", err)
|
||||
}
|
||||
@ -100,3 +161,19 @@ func dispatchReportInfoTask() {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newHTTPandGRPCMux(httpHandler http.Handler, grpcHandler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
natConfig := singleton.GetNATConfigByDomain(r.Host)
|
||||
if natConfig != nil {
|
||||
rpc.ServeNAT(w, r, natConfig)
|
||||
return
|
||||
}
|
||||
if r.ProtoMajor == 2 && r.Header.Get("Content-Type") == "application/grpc" &&
|
||||
strings.HasPrefix(r.URL.Path, "/"+proto.NezhaService_ServiceDesc.ServiceName) {
|
||||
grpcHandler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
httpHandler.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
@ -1,29 +1,78 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/peer"
|
||||
|
||||
"github.com/naiba/nezha/model"
|
||||
pb "github.com/naiba/nezha/proto"
|
||||
rpcService "github.com/naiba/nezha/service/rpc"
|
||||
"github.com/naiba/nezha/service/singleton"
|
||||
"github.com/hashicorp/go-uuid"
|
||||
"github.com/nezhahq/nezha/model"
|
||||
"github.com/nezhahq/nezha/pkg/utils"
|
||||
"github.com/nezhahq/nezha/proto"
|
||||
rpcService "github.com/nezhahq/nezha/service/rpc"
|
||||
"github.com/nezhahq/nezha/service/singleton"
|
||||
)
|
||||
|
||||
func ServeRPC(port uint) {
|
||||
server := grpc.NewServer()
|
||||
func ServeRPC() *grpc.Server {
|
||||
server := grpc.NewServer(grpc.ChainUnaryInterceptor(getRealIp, waf))
|
||||
rpcService.NezhaHandlerSingleton = rpcService.NewNezhaHandler()
|
||||
pb.RegisterNezhaServiceServer(server, rpcService.NezhaHandlerSingleton)
|
||||
listen, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
server.Serve(listen)
|
||||
proto.RegisterNezhaServiceServer(server, rpcService.NezhaHandlerSingleton)
|
||||
return server
|
||||
}
|
||||
|
||||
func DispatchTask(serviceSentinelDispatchBus <-chan model.Monitor) {
|
||||
func waf(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
|
||||
realip, _ := ctx.Value(model.CtxKeyRealIP{}).(string)
|
||||
if err := model.CheckIP(singleton.DB, realip); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handler(ctx, req)
|
||||
}
|
||||
|
||||
func getRealIp(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
|
||||
if singleton.Conf.RealIPHeader == "" {
|
||||
return handler(ctx, req)
|
||||
}
|
||||
|
||||
var ip string
|
||||
|
||||
if singleton.Conf.RealIPHeader == model.ConfigUsePeerIP {
|
||||
p, ok := peer.FromContext(ctx)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("peer not found")
|
||||
}
|
||||
addrPort, err := netip.ParseAddrPort(p.Addr.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ip = addrPort.Addr().String()
|
||||
} else {
|
||||
vals := metadata.ValueFromIncomingContext(ctx, singleton.Conf.RealIPHeader)
|
||||
if len(vals) == 0 {
|
||||
return nil, fmt.Errorf("real ip header not found")
|
||||
}
|
||||
var err error
|
||||
ip, err = utils.GetIPFromHeader(vals[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if singleton.Conf.Debug {
|
||||
log.Printf("NEZHA>> gRPC Real IP: %s", ip)
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, model.CtxKeyRealIP{}, ip)
|
||||
return handler(ctx, req)
|
||||
}
|
||||
|
||||
func DispatchTask(serviceSentinelDispatchBus <-chan model.Service) {
|
||||
workedServerIndex := 0
|
||||
for task := range serviceSentinelDispatchBus {
|
||||
round := 0
|
||||
@ -43,17 +92,17 @@ func DispatchTask(serviceSentinelDispatchBus <-chan model.Monitor) {
|
||||
continue
|
||||
}
|
||||
// 如果此任务不可使用此服务器请求,跳过这个服务器(有些 IPv6 only 开了 NAT64 的机器请求 IPv4 总会出问题)
|
||||
if (task.Cover == model.MonitorCoverAll && task.SkipServers[singleton.SortedServerList[workedServerIndex].ID]) ||
|
||||
(task.Cover == model.MonitorCoverIgnoreAll && !task.SkipServers[singleton.SortedServerList[workedServerIndex].ID]) {
|
||||
if (task.Cover == model.ServiceCoverAll && task.SkipServers[singleton.SortedServerList[workedServerIndex].ID]) ||
|
||||
(task.Cover == model.ServiceCoverIgnoreAll && !task.SkipServers[singleton.SortedServerList[workedServerIndex].ID]) {
|
||||
workedServerIndex++
|
||||
continue
|
||||
}
|
||||
if task.Cover == model.MonitorCoverIgnoreAll && task.SkipServers[singleton.SortedServerList[workedServerIndex].ID] {
|
||||
if task.Cover == model.ServiceCoverIgnoreAll && task.SkipServers[singleton.SortedServerList[workedServerIndex].ID] {
|
||||
singleton.SortedServerList[workedServerIndex].TaskStream.Send(task.PB())
|
||||
workedServerIndex++
|
||||
continue
|
||||
}
|
||||
if task.Cover == model.MonitorCoverAll && !task.SkipServers[singleton.SortedServerList[workedServerIndex].ID] {
|
||||
if task.Cover == model.ServiceCoverAll && !task.SkipServers[singleton.SortedServerList[workedServerIndex].ID] {
|
||||
singleton.SortedServerList[workedServerIndex].TaskStream.Send(task.PB())
|
||||
workedServerIndex++
|
||||
continue
|
||||
@ -76,7 +125,62 @@ func DispatchKeepalive() {
|
||||
continue
|
||||
}
|
||||
|
||||
singleton.SortedServerList[i].TaskStream.Send(&pb.Task{Type: model.TaskTypeKeepalive})
|
||||
singleton.SortedServerList[i].TaskStream.Send(&proto.Task{Type: model.TaskTypeKeepalive})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func ServeNAT(w http.ResponseWriter, r *http.Request, natConfig *model.NAT) {
|
||||
singleton.ServerLock.RLock()
|
||||
server := singleton.ServerList[natConfig.ServerID]
|
||||
singleton.ServerLock.RUnlock()
|
||||
if server == nil || server.TaskStream == nil {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
w.Write([]byte("server not found or not connected"))
|
||||
return
|
||||
}
|
||||
|
||||
streamId, err := uuid.GenerateUUID()
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
w.Write([]byte(fmt.Sprintf("stream id error: %v", err)))
|
||||
return
|
||||
}
|
||||
|
||||
rpcService.NezhaHandlerSingleton.CreateStream(streamId)
|
||||
defer rpcService.NezhaHandlerSingleton.CloseStream(streamId)
|
||||
|
||||
taskData, err := utils.Json.Marshal(model.TaskNAT{
|
||||
StreamID: streamId,
|
||||
Host: natConfig.Host,
|
||||
})
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
w.Write([]byte(fmt.Sprintf("task data error: %v", err)))
|
||||
return
|
||||
}
|
||||
|
||||
if err := server.TaskStream.Send(&proto.Task{
|
||||
Type: model.TaskTypeNAT,
|
||||
Data: string(taskData),
|
||||
}); err != nil {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
w.Write([]byte(fmt.Sprintf("send task error: %v", err)))
|
||||
return
|
||||
}
|
||||
|
||||
wWrapped, err := utils.NewRequestWrapper(r, w)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
w.Write([]byte(fmt.Sprintf("request wrapper error: %v", err)))
|
||||
return
|
||||
}
|
||||
|
||||
if err := rpcService.NezhaHandlerSingleton.UserConnected(streamId, wWrapped); err != nil {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
w.Write([]byte(fmt.Sprintf("user connected error: %v", err)))
|
||||
return
|
||||
}
|
||||
|
||||
rpcService.NezhaHandlerSingleton.StartStream(streamId, time.Second*10)
|
||||
}
|
||||
|
0
cmd/dashboard/user-dist/.gitkeep
Normal file
0
cmd/dashboard/user-dist/.gitkeep
Normal file
119
go.mod
119
go.mod
@ -1,94 +1,91 @@
|
||||
module github.com/naiba/nezha
|
||||
module github.com/nezhahq/nezha
|
||||
|
||||
go 1.21
|
||||
go 1.22.7
|
||||
|
||||
toolchain go1.23.1
|
||||
|
||||
require (
|
||||
code.cloudfoundry.org/bytefmt v0.0.0-20240425163905-bcdc1ad063ea
|
||||
code.gitea.io/sdk/gitea v0.18.0
|
||||
github.com/BurntSushi/toml v1.3.2
|
||||
github.com/coreos/go-oidc/v3 v3.11.0
|
||||
github.com/gin-contrib/pprof v1.4.0
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/google/go-github/v47 v47.1.0
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/appleboy/gin-jwt/v2 v2.10.0
|
||||
github.com/chai2010/gettext-go v1.0.3
|
||||
github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0
|
||||
github.com/gin-contrib/pprof v1.5.1
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/hashicorp/go-uuid v1.0.3
|
||||
github.com/jinzhu/copier v0.4.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/knadh/koanf/parsers/yaml v0.1.0
|
||||
github.com/knadh/koanf/providers/env v1.0.0
|
||||
github.com/knadh/koanf/providers/file v1.1.2
|
||||
github.com/knadh/koanf/v2 v2.1.2
|
||||
github.com/libdns/cloudflare v0.1.1
|
||||
github.com/libdns/libdns v0.2.2
|
||||
github.com/miekg/dns v1.1.62
|
||||
github.com/nezhahq/libdns-tencentcloud v0.0.0-20241029120103-889957240fff
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.0
|
||||
github.com/ory/graceful v0.1.3
|
||||
github.com/oschwald/maxminddb-golang v1.13.1
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.18.2
|
||||
github.com/swaggo/files v1.0.1
|
||||
github.com/swaggo/gin-swagger v1.6.0
|
||||
github.com/swaggo/swag v1.16.4
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/xanzy/go-gitlab v0.103.0
|
||||
golang.org/x/crypto v0.25.0
|
||||
golang.org/x/net v0.27.0
|
||||
golang.org/x/oauth2 v0.21.0
|
||||
golang.org/x/sync v0.7.0
|
||||
golang.org/x/text v0.16.0
|
||||
google.golang.org/grpc v1.63.0
|
||||
google.golang.org/protobuf v1.34.2
|
||||
gorm.io/driver/sqlite v1.5.5
|
||||
gorm.io/gorm v1.25.10
|
||||
sigs.k8s.io/yaml v1.4.0
|
||||
golang.org/x/crypto v0.29.0
|
||||
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f
|
||||
golang.org/x/net v0.31.0
|
||||
golang.org/x/sync v0.9.0
|
||||
google.golang.org/grpc v1.68.0
|
||||
google.golang.org/protobuf v1.35.2
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/sqlite v1.5.6
|
||||
gorm.io/gorm v1.25.12
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/bytedance/sonic v1.12.4 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.6 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-fed/httpsig v1.1.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/spec v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
||||
github.com/hashicorp/go-version v1.6.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.22.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||
github.com/knadh/koanf/maps v0.1.1 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/mod v0.18.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.22.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
golang.org/x/arch v0.12.0 // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/sys v0.27.0 // indirect
|
||||
golang.org/x/text v0.20.0 // indirect
|
||||
golang.org/x/tools v0.27.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect
|
||||
)
|
||||
|
317
go.sum
317
go.sum
@ -1,129 +1,115 @@
|
||||
code.cloudfoundry.org/bytefmt v0.0.0-20240425163905-bcdc1ad063ea h1:1tgMNDgo8PjpsHhlaxdibj28C0WyLeOW2SPJ7GGdc9A=
|
||||
code.cloudfoundry.org/bytefmt v0.0.0-20240425163905-bcdc1ad063ea/go.mod h1:3+xXJBOD8PsGHDqHedtCLalbaVJ+yi1OW+mXx9IcNxI=
|
||||
code.gitea.io/sdk/gitea v0.18.0 h1:+zZrwVmujIrgobt6wVBWCqITz6bn1aBjnCUHmpZrerI=
|
||||
code.gitea.io/sdk/gitea v0.18.0/go.mod h1:IG9xZJoltDNeDSW0qiF2Vqx5orMWa7OhVWrjvrd5NpI=
|
||||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
|
||||
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/appleboy/gin-jwt/v2 v2.10.0 h1:vOlGSly8oIGQiT8AcEh1nYMLYI1K9YvsZNVWM612xN0=
|
||||
github.com/appleboy/gin-jwt/v2 v2.10.0/go.mod h1:DvCh3V1Ma32/7kAsAHYQVyjsQMwG+wMXGpyCYLfHOJU=
|
||||
github.com/appleboy/gofight/v2 v2.1.2 h1:VOy3jow4vIK8BRQJoC/I9muxyYlJ2yb9ht2hZoS3rf4=
|
||||
github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw=
|
||||
github.com/bytedance/sonic v1.12.4 h1:9Csb3c9ZJhfUWeMtpCDCq6BUoH5ogfDFLUgQ/jG+R0k=
|
||||
github.com/bytedance/sonic v1.12.4/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
|
||||
github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/chai2010/gettext-go v1.0.3 h1:9liNh8t+u26xl5ddmWLmsOsdNLwkdRTg5AG+JnTiM80=
|
||||
github.com/chai2010/gettext-go v1.0.3/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
|
||||
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0 h1:aYo8nnk3ojoQkP5iErif5Xxv0Mo0Ga/FR5+ffl/7+Nk=
|
||||
github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0/go.mod h1:8AuBTZBRSFqEYBPYULd+NN474/zZBLP+6WeT5S9xlAc=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/gin-contrib/pprof v1.4.0 h1:XxiBSf5jWZ5i16lNOPbMTVdgHBdhfGRD5PZ1LWazzvg=
|
||||
github.com/gin-contrib/pprof v1.4.0/go.mod h1:RrehPJasUVBPK6yTUwOl8/NP6i0vbUgmxtis+Z5KE90=
|
||||
github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc=
|
||||
github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc=
|
||||
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||
github.com/gin-contrib/pprof v1.5.1 h1:Mzy+3HHtHbtwr4VewBTXZp/hR7pS6ZuZkueBIrQiLL4=
|
||||
github.com/gin-contrib/pprof v1.5.1/go.mod h1:uwzoF6FxdzJJGyMdcZB+VSuVjOBe1kSH+KMIvKGwvCQ=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
||||
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
||||
github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
|
||||
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
|
||||
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
|
||||
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-github/v47 v47.1.0 h1:Cacm/WxQBOa9lF0FT0EMjZ2BWMetQ1TQfyurn4yF1z8=
|
||||
github.com/google/go-github/v47 v47.1.0/go.mod h1:VPZBXNbFSJGjyjFRUKo9vZGawTajnWzC/YjGw/oFKi0=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg=
|
||||
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
|
||||
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
||||
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||
github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
|
||||
github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
|
||||
github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w=
|
||||
github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY=
|
||||
github.com/knadh/koanf/providers/env v1.0.0 h1:ufePaI9BnWH+ajuxGGiJ8pdTG0uLEUWC7/HDDPGLah0=
|
||||
github.com/knadh/koanf/providers/env v1.0.0/go.mod h1:mzFyRZueYhb37oPmC1HAv/oGEEuyvJDA98r3XAa8Gak=
|
||||
github.com/knadh/koanf/providers/file v1.1.2 h1:aCC36YGOgV5lTtAFz2qkgtWdeQsgfxUkxDOe+2nQY3w=
|
||||
github.com/knadh/koanf/providers/file v1.1.2/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI=
|
||||
github.com/knadh/koanf/v2 v2.1.2 h1:I2rtLRqXRy1p01m/utEtpZSSA6dcJbgGVuE27kW2PzQ=
|
||||
github.com/knadh/koanf/v2 v2.1.2/go.mod h1:Gphfaen0q1Fc1HTgJgSTC4oRX9R2R5ErYMZJy8fLJBo=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/libdns/cloudflare v0.1.1 h1:FVPfWwP8zZCqj268LZjmkDleXlHPlFU9KC4OJ3yn054=
|
||||
github.com/libdns/cloudflare v0.1.1/go.mod h1:9VK91idpOjg6v7/WbjkEW49bSCxj00ALesIFDhJ8PBU=
|
||||
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
|
||||
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@ -131,22 +117,14 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/nezhahq/libdns-tencentcloud v0.0.0-20241029120103-889957240fff h1:3WDsbsg3dmsRENYLUPGPTkEcWWmTvk41i+rM91AIIbY=
|
||||
github.com/nezhahq/libdns-tencentcloud v0.0.0-20241029120103-889957240fff/go.mod h1:k+cDtTbUY+UV56Aqv4Ahmc9bWmhpga9JJXZlAIPKBEc=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4=
|
||||
github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8=
|
||||
github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
|
||||
github.com/onsi/gomega v1.33.0 h1:snPCflnZrpMsy94p4lXVEkHo12lmPnc3vY5XBbreexE=
|
||||
github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY=
|
||||
github.com/ory/graceful v0.1.3 h1:FaeXcHZh168WzS+bqruqWEw/HgXWLdNv2nJ+fbhxbhc=
|
||||
github.com/ory/graceful v0.1.3/go.mod h1:4zFz687IAF7oNHHiB586U4iL+/4aV09o/PYLE34t2bA=
|
||||
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
|
||||
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@ -154,40 +132,25 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
||||
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||
github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
|
||||
github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
|
||||
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
|
||||
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
@ -199,87 +162,69 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/xanzy/go-gitlab v0.103.0 h1:J9pTQoq0GsEFqzd6srCM1QfdfKAxSNz6mT6ntrpNF2w=
|
||||
github.com/xanzy/go-gitlab v0.103.0/go.mod h1:ETg8tcj4OhrB84UEgeE8dSuV/0h4BBL1uOV/qK0vlyI=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg=
|
||||
golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
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-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
|
||||
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
|
||||
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
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.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
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-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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||
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/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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
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/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY=
|
||||
google.golang.org/grpc v1.63.0 h1:WjKe+dnvABXyPJMD7KDNLxtoGk5tgk+YFWN6cBWjZE8=
|
||||
google.golang.org/grpc v1.63.0/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o=
|
||||
golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 h1:zciRKQ4kBpFgpfC5QQCVtnnNAcLIqweL7plyZRQHVpI=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
|
||||
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
|
||||
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
|
||||
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
|
||||
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E=
|
||||
gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE=
|
||||
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
|
||||
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||
gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
|
||||
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
|
@ -1,9 +1,7 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/naiba/nezha/pkg/utils"
|
||||
"github.com/nezhahq/nezha/pkg/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@ -12,29 +10,18 @@ const (
|
||||
ModeOnetimeTrigger = 1
|
||||
)
|
||||
|
||||
type CycleTransferStats struct {
|
||||
Name string
|
||||
From time.Time
|
||||
To time.Time
|
||||
Max uint64
|
||||
Min uint64
|
||||
ServerName map[uint64]string
|
||||
Transfer map[uint64]uint64
|
||||
NextUpdate map[uint64]time.Time
|
||||
}
|
||||
|
||||
type AlertRule struct {
|
||||
Common
|
||||
Name string
|
||||
RulesRaw string
|
||||
Enable *bool
|
||||
TriggerMode int `gorm:"default:0"` // 触发模式: 0-始终触发(默认) 1-单次触发
|
||||
NotificationTag string // 该报警规则所在的通知组
|
||||
FailTriggerTasksRaw string `gorm:"default:'[]'"`
|
||||
RecoverTriggerTasksRaw string `gorm:"default:'[]'"`
|
||||
Rules []Rule `gorm:"-" json:"-"`
|
||||
FailTriggerTasks []uint64 `gorm:"-" json:"-"` // 失败时执行的触发任务id
|
||||
RecoverTriggerTasks []uint64 `gorm:"-" json:"-"` // 恢复时执行的触发任务id
|
||||
Name string `json:"name"`
|
||||
RulesRaw string `json:"-"`
|
||||
Enable *bool `json:"enable,omitempty"`
|
||||
TriggerMode uint8 `gorm:"default:0" json:"trigger_mode"` // 触发模式: 0-始终触发(默认) 1-单次触发
|
||||
NotificationGroupID uint64 `json:"notification_group_id"` // 该报警规则所在的通知组
|
||||
FailTriggerTasksRaw string `gorm:"default:'[]'" json:"-"`
|
||||
RecoverTriggerTasksRaw string `gorm:"default:'[]'" json:"-"`
|
||||
Rules []Rule `gorm:"-" json:"rules"`
|
||||
FailTriggerTasks []uint64 `gorm:"-" json:"fail_trigger_tasks"` // 失败时执行的触发任务id
|
||||
RecoverTriggerTasks []uint64 `gorm:"-" json:"recover_trigger_tasks"` // 恢复时执行的触发任务id
|
||||
}
|
||||
|
||||
func (r *AlertRule) BeforeSave(tx *gorm.DB) error {
|
||||
@ -74,55 +61,55 @@ func (r *AlertRule) Enabled() bool {
|
||||
return r.Enable != nil && *r.Enable
|
||||
}
|
||||
|
||||
// Snapshot 对传入的Server进行该报警规则下所有type的检查 返回包含每项检查结果的空接口
|
||||
func (r *AlertRule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server, db *gorm.DB) []interface{} {
|
||||
var point []interface{}
|
||||
for i := 0; i < len(r.Rules); i++ {
|
||||
point = append(point, r.Rules[i].Snapshot(cycleTransferStats, server, db))
|
||||
// Snapshot 对传入的Server进行该报警规则下所有type的检查 返回每项检查结果
|
||||
func (r *AlertRule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server, db *gorm.DB) []bool {
|
||||
point := make([]bool, 0, len(r.Rules))
|
||||
for _, rule := range r.Rules {
|
||||
point = append(point, rule.Snapshot(cycleTransferStats, server, db))
|
||||
}
|
||||
return point
|
||||
}
|
||||
|
||||
// Check 传入包含当前报警规则下所有type检查结果的空接口 返回报警持续时间与是否通过报警检查(通过则返回true)
|
||||
func (r *AlertRule) Check(points [][]interface{}) (int, bool) {
|
||||
var maxNum int // 报警持续时间
|
||||
var count int // 检查未通过的个数
|
||||
for i := 0; i < len(r.Rules); i++ {
|
||||
if r.Rules[i].IsTransferDurationRule() {
|
||||
// Check 传入包含当前报警规则下所有type检查结果 返回报警持续时间与是否通过报警检查(通过则返回true)
|
||||
func (r *AlertRule) Check(points [][]bool) (maxDuration int, passed bool) {
|
||||
failCount := 0 // 检查未通过的个数
|
||||
|
||||
for i, rule := range r.Rules {
|
||||
if rule.IsTransferDurationRule() {
|
||||
// 循环区间流量报警
|
||||
if maxNum < 1 {
|
||||
maxNum = 1
|
||||
if maxDuration < 1 {
|
||||
maxDuration = 1
|
||||
}
|
||||
for j := len(points[i]) - 1; j >= 0; j-- {
|
||||
if points[i][j] != nil {
|
||||
count++
|
||||
if !points[i][j] {
|
||||
failCount++
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 常规报警
|
||||
total := 0.0
|
||||
fail := 0.0
|
||||
num := int(r.Rules[i].Duration)
|
||||
if num > maxNum {
|
||||
maxNum = num
|
||||
duration := int(rule.Duration)
|
||||
if duration > maxDuration {
|
||||
maxDuration = duration
|
||||
}
|
||||
if len(points) < num {
|
||||
if len(points) < duration {
|
||||
continue
|
||||
}
|
||||
for j := len(points) - 1; j >= 0 && len(points)-num <= j; j-- {
|
||||
|
||||
total, fail := 0.0, 0.0
|
||||
for j := len(points) - duration; j < len(points); j++ {
|
||||
total++
|
||||
if points[j][i] != nil {
|
||||
if !points[j][i] {
|
||||
fail++
|
||||
}
|
||||
}
|
||||
// 当70%以上的采样点未通过规则判断时 才认为当前检查未通过
|
||||
if fail/total > 0.7 {
|
||||
count++
|
||||
failCount++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// 仅当所有检查均未通过时 返回false
|
||||
return maxNum, count != len(r.Rules)
|
||||
return maxDuration, failCount != len(r.Rules)
|
||||
}
|
||||
|
11
model/alertrule_api.go
Normal file
11
model/alertrule_api.go
Normal file
@ -0,0 +1,11 @@
|
||||
package model
|
||||
|
||||
type AlertRuleForm struct {
|
||||
Name string `json:"name" minLength:"1"`
|
||||
Rules []Rule `json:"rules"`
|
||||
FailTriggerTasks []uint64 `json:"fail_trigger_tasks"` // 失败时触发的任务id
|
||||
RecoverTriggerTasks []uint64 `json:"recover_trigger_tasks"` // 恢复时触发的任务id
|
||||
NotificationGroupID uint64 `json:"notification_group_id"`
|
||||
TriggerMode uint8 `json:"trigger_mode" default:"0"`
|
||||
Enable bool `json:"enable" validate:"optional"`
|
||||
}
|
30
model/api.go
30
model/api.go
@ -1,19 +1,21 @@
|
||||
package model
|
||||
|
||||
type ServiceItemResponse struct {
|
||||
Monitor *Monitor
|
||||
CurrentUp uint64
|
||||
CurrentDown uint64
|
||||
TotalUp uint64
|
||||
TotalDown uint64
|
||||
Delay *[30]float32
|
||||
Up *[30]int
|
||||
Down *[30]int
|
||||
const (
|
||||
ApiErrorUnauthorized = 10001
|
||||
)
|
||||
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
}
|
||||
|
||||
func (r ServiceItemResponse) TotalUptime() float32 {
|
||||
if r.TotalUp+r.TotalDown == 0 {
|
||||
return 0
|
||||
}
|
||||
return float32(r.TotalUp) / (float32(r.TotalUp + r.TotalDown)) * 100
|
||||
type CommonResponse[T any] struct {
|
||||
Success bool `json:"success,omitempty"`
|
||||
Data T `json:"data,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type LoginResponse struct {
|
||||
Token string `json:"token,omitempty"`
|
||||
Expire string `json:"expire,omitempty"`
|
||||
}
|
||||
|
@ -1,8 +0,0 @@
|
||||
package model
|
||||
|
||||
type ApiToken struct {
|
||||
Common
|
||||
UserID uint64 `json:"user_id"`
|
||||
Token string `json:"token"`
|
||||
Note string `json:"note"`
|
||||
}
|
@ -2,20 +2,21 @@ package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const CtxKeyAuthorizedUser = "ckau"
|
||||
const CtxKeyViewPasswordVerified = "ckvpv"
|
||||
const CtxKeyPreferredTheme = "ckpt"
|
||||
const CacheKeyOauth2State = "p:a:state"
|
||||
const (
|
||||
CtxKeyAuthorizedUser = "ckau"
|
||||
CtxKeyRealIPStr = "ckri"
|
||||
)
|
||||
|
||||
type CtxKeyRealIP struct{}
|
||||
|
||||
type Common struct {
|
||||
ID uint64 `gorm:"primaryKey"`
|
||||
CreatedAt time.Time `gorm:"index;<-:create"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
ID uint64 `gorm:"primaryKey" json:"id,omitempty"`
|
||||
CreatedAt time.Time `gorm:"index;<-:create" json:"created_at,omitempty"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at,omitempty"`
|
||||
// Do not use soft deletion
|
||||
// DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
|
212
model/config.go
212
model/config.go
@ -2,183 +2,111 @@ package model
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
kyaml "github.com/knadh/koanf/parsers/yaml"
|
||||
"github.com/knadh/koanf/providers/env"
|
||||
"github.com/knadh/koanf/providers/file"
|
||||
"github.com/knadh/koanf/v2"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
var Languages = map[string]string{
|
||||
"zh-CN": "简体中文",
|
||||
"zh-TW": "繁體中文",
|
||||
"en-US": "English",
|
||||
"es-ES": "Español",
|
||||
}
|
||||
|
||||
var Themes = map[string]string{
|
||||
"default": "Default",
|
||||
"daynight": "JackieSung DayNight",
|
||||
"mdui": "Neko Mdui",
|
||||
"hotaru": "Hotaru",
|
||||
"angel-kanade": "AngelKanade",
|
||||
"server-status": "ServerStatus",
|
||||
"custom": "Custom(local)",
|
||||
}
|
||||
|
||||
var DashboardThemes = map[string]string{
|
||||
"default": "Default",
|
||||
"custom": "Custom(local)",
|
||||
}
|
||||
|
||||
const (
|
||||
ConfigTypeGitHub = "github"
|
||||
ConfigTypeGitee = "gitee"
|
||||
ConfigTypeGitlab = "gitlab"
|
||||
ConfigTypeJihulab = "jihulab"
|
||||
ConfigTypeGitea = "gitea"
|
||||
ConfigTypeCloudflare = "cloudflare"
|
||||
ConfigTypeOidc = "oidc"
|
||||
"github.com/nezhahq/nezha/pkg/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
ConfigCoverAll = iota
|
||||
ConfigUsePeerIP = "NZ::Use-Peer-IP"
|
||||
ConfigCoverAll = iota
|
||||
ConfigCoverIgnoreAll
|
||||
)
|
||||
|
||||
type AgentConfig struct {
|
||||
HardDrivePartitionAllowlist []string
|
||||
NICAllowlist map[string]bool
|
||||
v *viper.Viper
|
||||
}
|
||||
|
||||
// Read 从给定的文件目录加载配置文件
|
||||
func (c *AgentConfig) Read(path string) error {
|
||||
c.v = viper.New()
|
||||
c.v.SetConfigFile(path)
|
||||
err := c.v.ReadInConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.v.Unmarshal(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *AgentConfig) Save() error {
|
||||
data, err := yaml.Marshal(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(c.v.ConfigFileUsed(), data, 0600)
|
||||
}
|
||||
|
||||
// Config 站点配置
|
||||
type Config struct {
|
||||
Debug bool // debug模式开关
|
||||
Language string // 系统语言,默认 zh-CN
|
||||
Site struct {
|
||||
Brand string // 站点名称
|
||||
CookieName string // 浏览器 Cookie 名称
|
||||
Theme string
|
||||
DashboardTheme string
|
||||
CustomCode string
|
||||
CustomCodeDashboard string
|
||||
ViewPassword string // 前台查看密码
|
||||
}
|
||||
Oauth2 struct {
|
||||
Type string
|
||||
Admin string // 管理员用户名列表
|
||||
AdminGroups string // 管理员用户组列表
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
Endpoint string
|
||||
OidcDisplayName string // for OIDC Display Name
|
||||
OidcIssuer string // for OIDC Issuer
|
||||
OidcLogoutURL string // for OIDC Logout URL
|
||||
OidcRegisterURL string // for OIDC Register URL
|
||||
OidcLoginClaim string // for OIDC Claim
|
||||
OidcGroupClaim string // for OIDC Group Claim
|
||||
OidcScopes string // for OIDC Scopes
|
||||
OidcAutoCreate bool // for OIDC Auto Create
|
||||
OidcAutoLogin bool // for OIDC Auto Login
|
||||
}
|
||||
HTTPPort uint
|
||||
GRPCPort uint
|
||||
GRPCHost string
|
||||
ProxyGRPCPort uint
|
||||
TLS bool
|
||||
Debug bool `mapstructure:"debug" json:"debug,omitempty"` // debug模式开关
|
||||
RealIPHeader string `mapstructure:"real_ip_header" json:"real_ip_header,omitempty"` // 真实IP
|
||||
|
||||
EnablePlainIPInNotification bool // 通知信息IP不打码
|
||||
DisableSwitchTemplateInFrontend bool // 前台禁用切换模板功能
|
||||
Language string `mapstructure:"language" json:"language"` // 系统语言,默认 zh_CN
|
||||
SiteName string `mapstructure:"site_name" json:"site_name"`
|
||||
JWTSecretKey string `mapstructure:"jwt_secret_key" json:"jwt_secret_key,omitempty"`
|
||||
AgentSecretKey string `mapstructure:"agent_secret_key" json:"agent_secret_key,omitempty"`
|
||||
ListenPort uint `mapstructure:"listen_port" json:"listen_port,omitempty"`
|
||||
InstallHost string `mapstructure:"install_host" json:"install_host,omitempty"`
|
||||
TLS bool `mapstructure:"tls" json:"tls,omitempty"`
|
||||
Location string `mapstructure:"location" json:"location,omitempty"` // 时区,默认为 Asia/Shanghai
|
||||
|
||||
EnablePlainIPInNotification bool `mapstructure:"enable_plain_ip_in_notification" json:"enable_plain_ip_in_notification,omitempty"` // 通知信息IP不打码
|
||||
|
||||
// IP变更提醒
|
||||
EnableIPChangeNotification bool
|
||||
IPChangeNotificationTag string
|
||||
Cover uint8 // 覆盖范围(0:提醒未被 IgnoredIPNotification 包含的所有服务器; 1:仅提醒被 IgnoredIPNotification 包含的服务器;)
|
||||
IgnoredIPNotification string // 特定服务器IP(多个服务器用逗号分隔)
|
||||
EnableIPChangeNotification bool `mapstructure:"enable_ip_change_notification" json:"enable_ip_change_notification,omitempty"`
|
||||
IPChangeNotificationGroupID uint64 `mapstructure:"ip_change_notification_group_id" json:"ip_change_notification_group_id"`
|
||||
Cover uint8 `mapstructure:"cover" json:"cover"` // 覆盖范围(0:提醒未被 IgnoredIPNotification 包含的所有服务器; 1:仅提醒被 IgnoredIPNotification 包含的服务器;)
|
||||
IgnoredIPNotification string `mapstructure:"ignored_ip_notification" json:"ignored_ip_notification,omitempty"` // 特定服务器IP(多个服务器用逗号分隔)
|
||||
|
||||
Location string // 时区,默认为 Asia/Shanghai
|
||||
IgnoredIPNotificationServerIDs map[uint64]bool `mapstructure:"ignored_ip_notification_server_ids" json:"ignored_ip_notification_server_ids,omitempty"` // [ServerID] -> bool(值为true代表当前ServerID在特定服务器列表内)
|
||||
AvgPingCount int `mapstructure:"avg_ping_count" json:"avg_ping_count,omitempty"`
|
||||
DNSServers string `mapstructure:"dns_servers" json:"dns_servers,omitempty"`
|
||||
|
||||
v *viper.Viper
|
||||
IgnoredIPNotificationServerIDs map[uint64]bool // [ServerID] -> bool(值为true代表当前ServerID在特定服务器列表内)
|
||||
MaxTCPPingValue int32
|
||||
AvgPingCount int
|
||||
CustomCode string `mapstructure:"custom_code" json:"custom_code,omitempty"`
|
||||
CustomCodeDashboard string `mapstructure:"custom_code_dashboard" json:"custom_code_dashboard,omitempty"`
|
||||
|
||||
DNSServers string
|
||||
k *koanf.Koanf `json:"-"`
|
||||
filePath string `json:"-"`
|
||||
}
|
||||
|
||||
// Read 读取配置文件并应用
|
||||
func (c *Config) Read(path string) error {
|
||||
c.v = viper.New()
|
||||
c.v.SetConfigFile(path)
|
||||
err := c.v.ReadInConfig()
|
||||
c.k = koanf.New(".")
|
||||
c.filePath = path
|
||||
|
||||
err := c.k.Load(env.Provider("NZ_", ".", func(s string) string {
|
||||
return strings.Replace(strings.ToLower(strings.TrimPrefix(s, "NZ_")), "_", ".", -1)
|
||||
}), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = c.v.Unmarshal(c)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
err = c.k.Load(file.Provider(path), kyaml.Parser())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = c.k.Unmarshal("", c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Site.Theme == "" {
|
||||
c.Site.Theme = "default"
|
||||
}
|
||||
if c.Site.DashboardTheme == "" {
|
||||
c.Site.DashboardTheme = "default"
|
||||
if c.ListenPort == 0 {
|
||||
c.ListenPort = 8008
|
||||
}
|
||||
if c.Language == "" {
|
||||
c.Language = "zh-CN"
|
||||
}
|
||||
if c.GRPCPort == 0 {
|
||||
c.GRPCPort = 5555
|
||||
}
|
||||
if c.EnableIPChangeNotification && c.IPChangeNotificationTag == "" {
|
||||
c.IPChangeNotificationTag = "default"
|
||||
c.Language = "zh_CN"
|
||||
}
|
||||
if c.Location == "" {
|
||||
c.Location = "Asia/Shanghai"
|
||||
}
|
||||
if c.MaxTCPPingValue == 0 {
|
||||
c.MaxTCPPingValue = 1000
|
||||
}
|
||||
if c.AvgPingCount == 0 {
|
||||
c.AvgPingCount = 2
|
||||
}
|
||||
if c.Oauth2.OidcScopes == "" {
|
||||
c.Oauth2.OidcScopes = "openid,profile,email"
|
||||
if c.JWTSecretKey == "" {
|
||||
c.JWTSecretKey, err = utils.GenerateRandomString(1024)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = c.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if c.Oauth2.OidcLoginClaim == "" {
|
||||
c.Oauth2.OidcLoginClaim = "sub"
|
||||
}
|
||||
if c.Oauth2.OidcDisplayName == "" {
|
||||
c.Oauth2.OidcDisplayName = "OIDC"
|
||||
}
|
||||
if c.Oauth2.OidcGroupClaim == "" {
|
||||
c.Oauth2.OidcGroupClaim = "groups"
|
||||
|
||||
if c.AgentSecretKey == "" {
|
||||
c.AgentSecretKey, err = utils.GenerateRandomString(32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = c.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
c.updateIgnoredIPNotificationID()
|
||||
@ -204,5 +132,11 @@ func (c *Config) Save() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(c.v.ConfigFileUsed(), data, 0600)
|
||||
|
||||
dir := filepath.Dir(c.filePath)
|
||||
if err := os.MkdirAll(dir, 0750); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(c.filePath, data, 0600)
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ package model
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/naiba/nezha/pkg/utils"
|
||||
"github.com/nezhahq/nezha/pkg/utils"
|
||||
"github.com/robfig/cron/v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@ -18,19 +18,28 @@ const (
|
||||
|
||||
type Cron struct {
|
||||
Common
|
||||
Name string
|
||||
TaskType uint8 `gorm:"default:0"` // 0:计划任务 1:触发任务
|
||||
Scheduler string //分钟 小时 天 月 星期
|
||||
Command string
|
||||
Servers []uint64 `gorm:"-"`
|
||||
PushSuccessful bool // 推送成功的通知
|
||||
NotificationTag string // 指定通知方式的分组
|
||||
LastExecutedAt time.Time // 最后一次执行时间
|
||||
LastResult bool // 最后一次执行结果
|
||||
Cover uint8 // 计划任务覆盖范围 (0:仅覆盖特定服务器 1:仅忽略特定服务器 2:由触发该计划任务的服务器执行)
|
||||
Name string `json:"name"`
|
||||
TaskType uint8 `gorm:"default:0" json:"task_type"` // 0:计划任务 1:触发任务
|
||||
Scheduler string `json:"scheduler"` // 分钟 小时 天 月 星期
|
||||
Command string `json:"command,omitempty"`
|
||||
Servers []uint64 `gorm:"-" json:"servers"`
|
||||
PushSuccessful bool `json:"push_successful,omitempty"` // 推送成功的通知
|
||||
NotificationGroupID uint64 `json:"notification_group_id"` // 指定通知方式的分组
|
||||
LastExecutedAt time.Time `json:"last_executed_at,omitempty"` // 最后一次执行时间
|
||||
LastResult bool `json:"last_result,omitempty"` // 最后一次执行结果
|
||||
Cover uint8 `json:"cover"` // 计划任务覆盖范围 (0:仅覆盖特定服务器 1:仅忽略特定服务器 2:由触发该计划任务的服务器执行)
|
||||
|
||||
CronJobID cron.EntryID `gorm:"-"`
|
||||
ServersRaw string
|
||||
CronJobID cron.EntryID `gorm:"-" json:"cron_job_id,omitempty"`
|
||||
ServersRaw string `json:"-"`
|
||||
}
|
||||
|
||||
func (c *Cron) BeforeSave(tx *gorm.DB) error {
|
||||
if data, err := utils.Json.Marshal(c.Servers); err != nil {
|
||||
return err
|
||||
} else {
|
||||
c.ServersRaw = string(data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cron) AfterFind(tx *gorm.DB) error {
|
||||
|
12
model/cron_api.go
Normal file
12
model/cron_api.go
Normal file
@ -0,0 +1,12 @@
|
||||
package model
|
||||
|
||||
type CronForm struct {
|
||||
TaskType uint8 `json:"task_type,omitempty" default:"0"` // 0:计划任务 1:触发任务
|
||||
Name string `json:"name,omitempty" minLength:"1"`
|
||||
Scheduler string `json:"scheduler,omitempty"`
|
||||
Command string `json:"command,omitempty" validate:"optional"`
|
||||
Servers []uint64 `json:"servers,omitempty"`
|
||||
Cover uint8 `json:"cover,omitempty" default:"0"`
|
||||
PushSuccessful bool `json:"push_successful,omitempty" validate:"optional"`
|
||||
NotificationGroupID uint64 `json:"notification_group_id,omitempty"`
|
||||
}
|
104
model/ddns.go
104
model/ddns.go
@ -1,100 +1,52 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/nezhahq/nezha/pkg/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
ProviderDummy = iota
|
||||
ProviderWebHook
|
||||
ProviderCloudflare
|
||||
ProviderTencentCloud
|
||||
ProviderDummy = "dummy"
|
||||
ProviderWebHook = "webhook"
|
||||
ProviderCloudflare = "cloudflare"
|
||||
ProviderTencentCloud = "tencentcloud"
|
||||
)
|
||||
|
||||
const (
|
||||
_Dummy = "dummy"
|
||||
_WebHook = "webhook"
|
||||
_Cloudflare = "cloudflare"
|
||||
_TencentCloud = "tencentcloud"
|
||||
)
|
||||
|
||||
var ProviderMap = map[uint8]string{
|
||||
ProviderDummy: _Dummy,
|
||||
ProviderWebHook: _WebHook,
|
||||
ProviderCloudflare: _Cloudflare,
|
||||
ProviderTencentCloud: _TencentCloud,
|
||||
}
|
||||
|
||||
var ProviderList = []DDNSProvider{
|
||||
{
|
||||
Name: _Dummy,
|
||||
ID: ProviderDummy,
|
||||
},
|
||||
{
|
||||
Name: _Cloudflare,
|
||||
ID: ProviderCloudflare,
|
||||
AccessSecret: true,
|
||||
},
|
||||
{
|
||||
Name: _TencentCloud,
|
||||
ID: ProviderTencentCloud,
|
||||
AccessID: true,
|
||||
AccessSecret: true,
|
||||
},
|
||||
// Least frequently used, always place this at the end
|
||||
{
|
||||
Name: _WebHook,
|
||||
ID: ProviderWebHook,
|
||||
AccessID: true,
|
||||
AccessSecret: true,
|
||||
WebhookURL: true,
|
||||
WebhookMethod: true,
|
||||
WebhookRequestType: true,
|
||||
WebhookRequestBody: true,
|
||||
WebhookHeaders: true,
|
||||
},
|
||||
var ProviderList = []string{
|
||||
ProviderDummy, ProviderWebHook, ProviderCloudflare, ProviderTencentCloud,
|
||||
}
|
||||
|
||||
type DDNSProfile struct {
|
||||
Common
|
||||
EnableIPv4 *bool
|
||||
EnableIPv6 *bool
|
||||
MaxRetries uint64
|
||||
Name string
|
||||
Provider uint8
|
||||
AccessID string
|
||||
AccessSecret string
|
||||
WebhookURL string
|
||||
WebhookMethod uint8
|
||||
WebhookRequestType uint8
|
||||
WebhookRequestBody string
|
||||
WebhookHeaders string
|
||||
|
||||
Domains []string `gorm:"-"`
|
||||
DomainsRaw string
|
||||
EnableIPv4 *bool `json:"enable_ipv4,omitempty"`
|
||||
EnableIPv6 *bool `json:"enable_ipv6,omitempty"`
|
||||
MaxRetries uint64 `json:"max_retries"`
|
||||
Name string `json:"name"`
|
||||
Provider string `json:"provider"`
|
||||
AccessID string `json:"access_id,omitempty"`
|
||||
AccessSecret string `json:"access_secret,omitempty"`
|
||||
WebhookURL string `json:"webhook_url,omitempty"`
|
||||
WebhookMethod uint8 `json:"webhook_method,omitempty"`
|
||||
WebhookRequestType uint8 `json:"webhook_request_type,omitempty"`
|
||||
WebhookRequestBody string `json:"webhook_request_body,omitempty"`
|
||||
WebhookHeaders string `json:"webhook_headers,omitempty"`
|
||||
Domains []string `json:"domains" gorm:"-"`
|
||||
DomainsRaw string `json:"-"`
|
||||
}
|
||||
|
||||
func (d DDNSProfile) TableName() string {
|
||||
return "ddns"
|
||||
}
|
||||
|
||||
func (d *DDNSProfile) AfterFind(tx *gorm.DB) error {
|
||||
if d.DomainsRaw != "" {
|
||||
d.Domains = strings.Split(d.DomainsRaw, ",")
|
||||
func (d *DDNSProfile) BeforeSave(tx *gorm.DB) error {
|
||||
if data, err := utils.Json.Marshal(d.Domains); err != nil {
|
||||
return err
|
||||
} else {
|
||||
d.DomainsRaw = string(data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type DDNSProvider struct {
|
||||
Name string
|
||||
ID uint8
|
||||
AccessID bool
|
||||
AccessSecret bool
|
||||
WebhookURL bool
|
||||
WebhookMethod bool
|
||||
WebhookRequestType bool
|
||||
WebhookRequestBody bool
|
||||
WebhookHeaders bool
|
||||
func (d *DDNSProfile) AfterFind(tx *gorm.DB) error {
|
||||
return utils.Json.Unmarshal([]byte(d.DomainsRaw), &d.Domains)
|
||||
}
|
||||
|
17
model/ddns_api.go
Normal file
17
model/ddns_api.go
Normal file
@ -0,0 +1,17 @@
|
||||
package model
|
||||
|
||||
type DDNSForm struct {
|
||||
MaxRetries uint64 `json:"max_retries,omitempty" default:"3"`
|
||||
EnableIPv4 bool `json:"enable_ipv4,omitempty" validate:"optional"`
|
||||
EnableIPv6 bool `json:"enable_ipv6,omitempty" validate:"optional"`
|
||||
Name string `json:"name,omitempty" minLength:"1"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
Domains []string `json:"domains,omitempty"`
|
||||
AccessID string `json:"access_id,omitempty" validate:"optional"`
|
||||
AccessSecret string `json:"access_secret,omitempty" validate:"optional"`
|
||||
WebhookURL string `json:"webhook_url,omitempty" validate:"optional"`
|
||||
WebhookMethod uint8 `json:"webhook_method,omitempty" validate:"optional" default:"1"`
|
||||
WebhookRequestType uint8 `json:"webhook_request_type,omitempty" validate:"optional" default:"1"`
|
||||
WebhookRequestBody string `json:"webhook_request_body,omitempty" validate:"optional"`
|
||||
WebhookHeaders string `json:"webhook_headers,omitempty" validate:"optional"`
|
||||
}
|
5
model/fm_api.go
Normal file
5
model/fm_api.go
Normal file
@ -0,0 +1,5 @@
|
||||
package model
|
||||
|
||||
type CreateFMResponse struct {
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
pb "github.com/naiba/nezha/proto"
|
||||
"fmt"
|
||||
|
||||
pb "github.com/nezhahq/nezha/proto"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -16,23 +18,23 @@ type SensorTemperature struct {
|
||||
}
|
||||
|
||||
type HostState struct {
|
||||
CPU float64
|
||||
MemUsed uint64
|
||||
SwapUsed uint64
|
||||
DiskUsed uint64
|
||||
NetInTransfer uint64
|
||||
NetOutTransfer uint64
|
||||
NetInSpeed uint64
|
||||
NetOutSpeed uint64
|
||||
Uptime uint64
|
||||
Load1 float64
|
||||
Load5 float64
|
||||
Load15 float64
|
||||
TcpConnCount uint64
|
||||
UdpConnCount uint64
|
||||
ProcessCount uint64
|
||||
Temperatures []SensorTemperature
|
||||
GPU float64
|
||||
CPU float64 `json:"cpu,omitempty"`
|
||||
MemUsed uint64 `json:"mem_used,omitempty"`
|
||||
SwapUsed uint64 `json:"swap_used,omitempty"`
|
||||
DiskUsed uint64 `json:"disk_used,omitempty"`
|
||||
NetInTransfer uint64 `json:"net_in_transfer,omitempty"`
|
||||
NetOutTransfer uint64 `json:"net_out_transfer,omitempty"`
|
||||
NetInSpeed uint64 `json:"net_in_speed,omitempty"`
|
||||
NetOutSpeed uint64 `json:"net_out_speed,omitempty"`
|
||||
Uptime uint64 `json:"uptime,omitempty"`
|
||||
Load1 float64 `json:"load_1,omitempty"`
|
||||
Load5 float64 `json:"load_5,omitempty"`
|
||||
Load15 float64 `json:"load_15,omitempty"`
|
||||
TcpConnCount uint64 `json:"tcp_conn_count,omitempty"`
|
||||
UdpConnCount uint64 `json:"udp_conn_count,omitempty"`
|
||||
ProcessCount uint64 `json:"process_count,omitempty"`
|
||||
Temperatures []SensorTemperature `json:"temperatures,omitempty"`
|
||||
GPU []float64 `json:"gpu,omitempty"`
|
||||
}
|
||||
|
||||
func (s *HostState) PB() *pb.State {
|
||||
@ -96,19 +98,17 @@ func PB2State(s *pb.State) HostState {
|
||||
}
|
||||
|
||||
type Host struct {
|
||||
Platform string
|
||||
PlatformVersion string
|
||||
CPU []string
|
||||
MemTotal uint64
|
||||
DiskTotal uint64
|
||||
SwapTotal uint64
|
||||
Arch string
|
||||
Virtualization string
|
||||
BootTime uint64
|
||||
IP string `json:"-"`
|
||||
CountryCode string
|
||||
Version string
|
||||
GPU []string
|
||||
Platform string `json:"platform,omitempty"`
|
||||
PlatformVersion string `json:"platform_version,omitempty"`
|
||||
CPU []string `json:"cpu,omitempty"`
|
||||
MemTotal uint64 `json:"mem_total,omitempty"`
|
||||
DiskTotal uint64 `json:"disk_total,omitempty"`
|
||||
SwapTotal uint64 `json:"swap_total,omitempty"`
|
||||
Arch string `json:"arch,omitempty"`
|
||||
Virtualization string `json:"virtualization,omitempty"`
|
||||
BootTime uint64 `json:"boot_time,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
GPU []string `json:"gpu,omitempty"`
|
||||
}
|
||||
|
||||
func (h *Host) PB() *pb.Host {
|
||||
@ -122,8 +122,6 @@ func (h *Host) PB() *pb.Host {
|
||||
Arch: h.Arch,
|
||||
Virtualization: h.Virtualization,
|
||||
BootTime: h.BootTime,
|
||||
Ip: h.IP,
|
||||
CountryCode: h.CountryCode,
|
||||
Version: h.Version,
|
||||
Gpu: h.GPU,
|
||||
}
|
||||
@ -140,9 +138,36 @@ func PB2Host(h *pb.Host) Host {
|
||||
Arch: h.GetArch(),
|
||||
Virtualization: h.GetVirtualization(),
|
||||
BootTime: h.GetBootTime(),
|
||||
IP: h.GetIp(),
|
||||
CountryCode: h.GetCountryCode(),
|
||||
Version: h.GetVersion(),
|
||||
GPU: h.GetGpu(),
|
||||
}
|
||||
}
|
||||
|
||||
type IP struct {
|
||||
IPv4Addr string `json:"ipv4_addr,omitempty"`
|
||||
IPv6Addr string `json:"ipv6_addr,omitempty"`
|
||||
}
|
||||
|
||||
func (p *IP) Join() string {
|
||||
if p.IPv4Addr != "" && p.IPv6Addr != "" {
|
||||
return fmt.Sprintf("%s/%s", p.IPv4Addr, p.IPv6Addr)
|
||||
} else if p.IPv4Addr != "" {
|
||||
return p.IPv4Addr
|
||||
}
|
||||
return p.IPv6Addr
|
||||
}
|
||||
|
||||
type GeoIP struct {
|
||||
IP IP `json:"ip,omitempty"`
|
||||
CountryCode string `json:"country_code,omitempty"`
|
||||
}
|
||||
|
||||
func PB2GeoIP(p *pb.GeoIP) GeoIP {
|
||||
pbIP := p.GetIp()
|
||||
return GeoIP{
|
||||
IP: IP{
|
||||
IPv4Addr: pbIP.GetIpv4(),
|
||||
IPv6Addr: pbIP.GetIpv6(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
142
model/monitor.go
142
model/monitor.go
@ -1,142 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/naiba/nezha/pkg/utils"
|
||||
pb "github.com/naiba/nezha/proto"
|
||||
)
|
||||
|
||||
const (
|
||||
_ = iota
|
||||
TaskTypeHTTPGet
|
||||
TaskTypeICMPPing
|
||||
TaskTypeTCPPing
|
||||
TaskTypeCommand
|
||||
TaskTypeTerminal
|
||||
TaskTypeUpgrade
|
||||
TaskTypeKeepalive
|
||||
TaskTypeTerminalGRPC
|
||||
TaskTypeNAT
|
||||
TaskTypeReportHostInfo
|
||||
TaskTypeFM
|
||||
)
|
||||
|
||||
type TerminalTask struct {
|
||||
StreamID string
|
||||
}
|
||||
|
||||
type TaskNAT struct {
|
||||
StreamID string
|
||||
Host string
|
||||
}
|
||||
|
||||
type TaskFM struct {
|
||||
StreamID string
|
||||
}
|
||||
|
||||
const (
|
||||
MonitorCoverAll = iota
|
||||
MonitorCoverIgnoreAll
|
||||
)
|
||||
|
||||
type Monitor struct {
|
||||
Common
|
||||
Name string
|
||||
Type uint8
|
||||
Target string
|
||||
SkipServersRaw string
|
||||
Duration uint64
|
||||
Notify bool
|
||||
NotificationTag string // 当前服务监控所属的通知组
|
||||
Cover uint8
|
||||
|
||||
EnableTriggerTask bool `gorm:"default: false"`
|
||||
EnableShowInService bool `gorm:"default: false"`
|
||||
FailTriggerTasksRaw string `gorm:"default:'[]'"`
|
||||
RecoverTriggerTasksRaw string `gorm:"default:'[]'"`
|
||||
FailTriggerTasks []uint64 `gorm:"-" json:"-"` // 失败时执行的触发任务id
|
||||
RecoverTriggerTasks []uint64 `gorm:"-" json:"-"` // 恢复时执行的触发任务id
|
||||
|
||||
MinLatency float32
|
||||
MaxLatency float32
|
||||
LatencyNotify bool
|
||||
|
||||
SkipServers map[uint64]bool `gorm:"-" json:"-"`
|
||||
CronJobID cron.EntryID `gorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
func (m *Monitor) PB() *pb.Task {
|
||||
return &pb.Task{
|
||||
Id: m.ID,
|
||||
Type: uint64(m.Type),
|
||||
Data: m.Target,
|
||||
}
|
||||
}
|
||||
|
||||
// CronSpec 返回服务监控请求间隔对应的 cron 表达式
|
||||
func (m *Monitor) CronSpec() string {
|
||||
if m.Duration == 0 {
|
||||
// 默认间隔 30 秒
|
||||
m.Duration = 30
|
||||
}
|
||||
return fmt.Sprintf("@every %ds", m.Duration)
|
||||
}
|
||||
|
||||
func (m *Monitor) BeforeSave(tx *gorm.DB) error {
|
||||
|
||||
if data, err := utils.Json.Marshal(m.FailTriggerTasks); err != nil {
|
||||
return err
|
||||
} else {
|
||||
m.FailTriggerTasksRaw = string(data)
|
||||
}
|
||||
if data, err := utils.Json.Marshal(m.RecoverTriggerTasks); err != nil {
|
||||
return err
|
||||
} else {
|
||||
m.RecoverTriggerTasksRaw = string(data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Monitor) AfterFind(tx *gorm.DB) error {
|
||||
m.SkipServers = make(map[uint64]bool)
|
||||
var skipServers []uint64
|
||||
if err := utils.Json.Unmarshal([]byte(m.SkipServersRaw), &skipServers); err != nil {
|
||||
log.Println("NEZHA>> Monitor.AfterFind:", err)
|
||||
return nil
|
||||
}
|
||||
for i := 0; i < len(skipServers); i++ {
|
||||
m.SkipServers[skipServers[i]] = true
|
||||
}
|
||||
|
||||
// 加载触发任务列表
|
||||
if err := utils.Json.Unmarshal([]byte(m.FailTriggerTasksRaw), &m.FailTriggerTasks); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := utils.Json.Unmarshal([]byte(m.RecoverTriggerTasksRaw), &m.RecoverTriggerTasks); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsServiceSentinelNeeded 判断该任务类型是否需要进行服务监控 需要则返回true
|
||||
func IsServiceSentinelNeeded(t uint64) bool {
|
||||
return t != TaskTypeCommand && t != TaskTypeTerminalGRPC && t != TaskTypeUpgrade
|
||||
}
|
||||
|
||||
func (m *Monitor) InitSkipServers() error {
|
||||
var skipServers []uint64
|
||||
if err := utils.Json.Unmarshal([]byte(m.SkipServersRaw), &skipServers); err != nil {
|
||||
return err
|
||||
}
|
||||
m.SkipServers = make(map[uint64]bool)
|
||||
for i := 0; i < len(skipServers); i++ {
|
||||
m.SkipServers[skipServers[i]] = true
|
||||
}
|
||||
return nil
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// MonitorHistory 历史监控记录
|
||||
type MonitorHistory struct {
|
||||
ID uint64 `gorm:"primaryKey"`
|
||||
CreatedAt time.Time `gorm:"index;<-:create;index:idx_server_id_created_at_monitor_id_avg_delay"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
MonitorID uint64 `gorm:"index:idx_server_id_created_at_monitor_id_avg_delay"`
|
||||
ServerID uint64 `gorm:"index:idx_server_id_created_at_monitor_id_avg_delay"`
|
||||
AvgDelay float32 `gorm:"index:idx_server_id_created_at_monitor_id_avg_delay"` // 平均延迟,毫秒
|
||||
Up uint64 // 检查状态良好计数
|
||||
Down uint64 // 检查状态异常计数
|
||||
Data string
|
||||
}
|
@ -2,8 +2,8 @@ package model
|
||||
|
||||
type NAT struct {
|
||||
Common
|
||||
Name string
|
||||
ServerID uint64
|
||||
Host string
|
||||
Domain string `gorm:"unique"`
|
||||
Name string `json:"name"`
|
||||
ServerID uint64 `json:"server_id"`
|
||||
Host string `json:"host"`
|
||||
Domain string `json:"domain" gorm:"unique"`
|
||||
}
|
||||
|
8
model/nat_api.go
Normal file
8
model/nat_api.go
Normal file
@ -0,0 +1,8 @@
|
||||
package model
|
||||
|
||||
type NATForm struct {
|
||||
Name string `json:"name,omitempty" minLength:"1"`
|
||||
ServerID uint64 `json:"server_id,omitempty"`
|
||||
Host string `json:"host,omitempty"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
}
|
@ -9,7 +9,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/naiba/nezha/pkg/utils"
|
||||
"github.com/nezhahq/nezha/pkg/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -32,14 +32,13 @@ type NotificationServerBundle struct {
|
||||
|
||||
type Notification struct {
|
||||
Common
|
||||
Name string
|
||||
Tag string // 分组名
|
||||
URL string
|
||||
RequestMethod int
|
||||
RequestType int
|
||||
RequestHeader string `gorm:"type:longtext" `
|
||||
RequestBody string `gorm:"type:longtext" `
|
||||
VerifySSL *bool
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
RequestMethod uint8 `json:"request_method"`
|
||||
RequestType uint8 `json:"request_type"`
|
||||
RequestHeader string `json:"request_header" gorm:"type:longtext"`
|
||||
RequestBody string `json:"request_body" gorm:"type:longtext"`
|
||||
VerifyTLS *bool `json:"verify_tls,omitempty"`
|
||||
}
|
||||
|
||||
func (ns *NotificationServerBundle) reqURL(message string) string {
|
||||
@ -112,7 +111,7 @@ func (n *Notification) setRequestHeader(req *http.Request) error {
|
||||
func (ns *NotificationServerBundle) Send(message string) error {
|
||||
var client *http.Client
|
||||
n := ns.Notification
|
||||
if n.VerifySSL != nil && *n.VerifySSL {
|
||||
if n.VerifyTLS != nil && *n.VerifyTLS {
|
||||
client = utils.HttpClient
|
||||
} else {
|
||||
client = utils.HttpClientSkipTlsVerify
|
||||
@ -194,7 +193,7 @@ func (ns *NotificationServerBundle) replaceParamsInString(str string, message st
|
||||
str = strings.ReplaceAll(str, "#SERVER.UDPCONNCOUNT#", mod(fmt.Sprintf("%d", ns.Server.State.UdpConnCount)))
|
||||
|
||||
var ipv4, ipv6, validIP string
|
||||
ipList := strings.Split(ns.Server.Host.IP, "/")
|
||||
ipList := strings.Split(ns.Server.GeoIP.IP.Join(), "/")
|
||||
if len(ipList) > 1 {
|
||||
// 双栈
|
||||
ipv4 = ipList[0]
|
||||
@ -202,7 +201,7 @@ func (ns *NotificationServerBundle) replaceParamsInString(str string, message st
|
||||
validIP = ipv4
|
||||
} else if len(ipList) == 1 {
|
||||
// 仅ipv4|ipv6
|
||||
if strings.Contains(ipList[0], ":") {
|
||||
if strings.IndexByte(ipList[0], ':') != -1 {
|
||||
ipv6 = ipList[0]
|
||||
validIP = ipv6
|
||||
} else {
|
||||
|
12
model/notification_api.go
Normal file
12
model/notification_api.go
Normal file
@ -0,0 +1,12 @@
|
||||
package model
|
||||
|
||||
type NotificationForm struct {
|
||||
Name string `json:"name,omitempty" minLength:"1"`
|
||||
URL string `json:"url,omitempty"`
|
||||
RequestMethod uint8 `json:"request_method,omitempty"`
|
||||
RequestType uint8 `json:"request_type,omitempty"`
|
||||
RequestHeader string `json:"request_header,omitempty"`
|
||||
RequestBody string `json:"request_body,omitempty"`
|
||||
VerifyTLS bool `json:"verify_tls,omitempty" validate:"optional"`
|
||||
SkipCheck bool `json:"skip_check,omitempty" validate:"optional"`
|
||||
}
|
6
model/notification_group.go
Normal file
6
model/notification_group.go
Normal file
@ -0,0 +1,6 @@
|
||||
package model
|
||||
|
||||
type NotificationGroup struct {
|
||||
Common
|
||||
Name string `json:"name"`
|
||||
}
|
11
model/notification_group_api.go
Normal file
11
model/notification_group_api.go
Normal file
@ -0,0 +1,11 @@
|
||||
package model
|
||||
|
||||
type NotificationGroupForm struct {
|
||||
Name string `json:"name" minLength:"1"`
|
||||
Notifications []uint64 `json:"notifications"`
|
||||
}
|
||||
|
||||
type NotificationGroupResponseItem struct {
|
||||
Group NotificationGroup `json:"group"`
|
||||
Notifications []uint64 `json:"notifications"`
|
||||
}
|
7
model/notification_group_notification.go
Normal file
7
model/notification_group_notification.go
Normal file
@ -0,0 +1,7 @@
|
||||
package model
|
||||
|
||||
type NotificationGroupNotification struct {
|
||||
Common
|
||||
NotificationGroupID uint64 `json:"notification_group_id" gorm:"uniqueIndex:idx_notification_group_notification"`
|
||||
NotificationID uint64 `json:"notification_id" gorm:"uniqueIndex:idx_notification_group_notification"`
|
||||
}
|
@ -17,8 +17,8 @@ type testSt struct {
|
||||
url string
|
||||
body string
|
||||
header string
|
||||
reqType int
|
||||
reqMethod int
|
||||
reqType uint8
|
||||
reqMethod uint8
|
||||
expectURL string
|
||||
expectBody string
|
||||
expectMethod string
|
||||
@ -37,8 +37,6 @@ func execCase(t *testing.T, item testSt) {
|
||||
server := Server{
|
||||
Common: Common{},
|
||||
Name: "ServerName",
|
||||
Tag: "",
|
||||
Secret: "",
|
||||
Note: "",
|
||||
DisplayIndex: 0,
|
||||
Host: &Host{
|
||||
@ -51,8 +49,6 @@ func execCase(t *testing.T, item testSt) {
|
||||
Arch: "",
|
||||
Virtualization: "",
|
||||
BootTime: 0,
|
||||
IP: "1.1.1.1",
|
||||
CountryCode: "",
|
||||
Version: "",
|
||||
},
|
||||
State: &HostState{
|
||||
@ -72,6 +68,12 @@ func execCase(t *testing.T, item testSt) {
|
||||
UdpConnCount: 0,
|
||||
ProcessCount: 0,
|
||||
},
|
||||
GeoIP: &GeoIP{
|
||||
IP: IP{
|
||||
IPv4Addr: "1.1.1.1",
|
||||
},
|
||||
CountryCode: "",
|
||||
},
|
||||
LastActive: time.Time{},
|
||||
TaskClose: nil,
|
||||
TaskStream: nil,
|
||||
|
104
model/rule.go
104
model/rule.go
@ -7,7 +7,7 @@ import (
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/naiba/nezha/pkg/utils"
|
||||
"github.com/nezhahq/nezha/pkg/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -23,19 +23,19 @@ type Rule struct {
|
||||
// 指标类型,cpu、memory、swap、disk、net_in_speed、net_out_speed
|
||||
// net_all_speed、transfer_in、transfer_out、transfer_all、offline
|
||||
// transfer_in_cycle、transfer_out_cycle、transfer_all_cycle
|
||||
Type string `json:"type,omitempty"`
|
||||
Min float64 `json:"min,omitempty"` // 最小阈值 (百分比、字节 kb ÷ 1024)
|
||||
Max float64 `json:"max,omitempty"` // 最大阈值 (百分比、字节 kb ÷ 1024)
|
||||
CycleStart *time.Time `json:"cycle_start,omitempty"` // 流量统计的开始时间
|
||||
CycleInterval uint64 `json:"cycle_interval,omitempty"` // 流量统计周期
|
||||
CycleUnit string `json:"cycle_unit,omitempty"` // 流量统计周期单位,默认hour,可选(hour, day, week, month, year)
|
||||
Duration uint64 `json:"duration,omitempty"` // 持续时间 (秒)
|
||||
Cover uint64 `json:"cover,omitempty"` // 覆盖范围 RuleCoverAll/IgnoreAll
|
||||
Ignore map[uint64]bool `json:"ignore,omitempty"` // 覆盖范围的排除
|
||||
Type string `json:"type"`
|
||||
Min float64 `json:"min,omitempty" validate:"optional"` // 最小阈值 (百分比、字节 kb ÷ 1024)
|
||||
Max float64 `json:"max,omitempty" validate:"optional"` // 最大阈值 (百分比、字节 kb ÷ 1024)
|
||||
CycleStart *time.Time `json:"cycle_start,omitempty" validate:"optional"` // 流量统计的开始时间
|
||||
CycleInterval uint64 `json:"cycle_interval,omitempty" validate:"optional"` // 流量统计周期
|
||||
CycleUnit string `json:"cycle_unit,omitempty" enums:"hour,day,week,month,year" validate:"optional" default:"hour"` // 流量统计周期单位,默认hour,可选(hour, day, week, month, year)
|
||||
Duration uint64 `json:"duration,omitempty" validate:"optional"` // 持续时间 (秒)
|
||||
Cover uint64 `json:"cover"` // 覆盖范围 RuleCoverAll/IgnoreAll
|
||||
Ignore map[uint64]bool `json:"ignore,omitempty" validate:"optional"` // 覆盖范围的排除
|
||||
|
||||
// 只作为缓存使用,记录下次该检测的时间
|
||||
NextTransferAt map[uint64]time.Time `json:"-"`
|
||||
LastCycleStatus map[uint64]interface{} `json:"-"`
|
||||
NextTransferAt map[uint64]time.Time `json:"-"`
|
||||
LastCycleStatus map[uint64]bool `json:"-"`
|
||||
}
|
||||
|
||||
func percentage(used, total uint64) float64 {
|
||||
@ -45,15 +45,15 @@ func percentage(used, total uint64) float64 {
|
||||
return float64(used) * 100 / float64(total)
|
||||
}
|
||||
|
||||
// Snapshot 未通过规则返回 struct{}{}, 通过返回 nil
|
||||
func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server, db *gorm.DB) interface{} {
|
||||
// Snapshot 未通过规则返回 false, 通过返回 true
|
||||
func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server, db *gorm.DB) bool {
|
||||
// 监控全部但是排除了此服务器
|
||||
if u.Cover == RuleCoverAll && u.Ignore[server.ID] {
|
||||
return nil
|
||||
return true
|
||||
}
|
||||
// 忽略全部但是指定监控了此服务器
|
||||
if u.Cover == RuleCoverIgnoreAll && !u.Ignore[server.ID] {
|
||||
return nil
|
||||
return true
|
||||
}
|
||||
|
||||
// 循环区间流量检测 · 短期无需重复检测
|
||||
@ -66,8 +66,8 @@ func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server,
|
||||
switch u.Type {
|
||||
case "cpu":
|
||||
src = float64(server.State.CPU)
|
||||
case "gpu":
|
||||
src = float64(server.State.GPU)
|
||||
case "gpu_max":
|
||||
src = slices.Max(server.State.GPU)
|
||||
case "memory":
|
||||
src = percentage(server.State.MemUsed, server.Host.MemTotal)
|
||||
case "swap":
|
||||
@ -147,13 +147,13 @@ func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server,
|
||||
u.NextTransferAt = make(map[uint64]time.Time)
|
||||
}
|
||||
if u.LastCycleStatus == nil {
|
||||
u.LastCycleStatus = make(map[uint64]interface{})
|
||||
u.LastCycleStatus = make(map[uint64]bool)
|
||||
}
|
||||
u.NextTransferAt[server.ID] = time.Now().Add(time.Second * time.Duration(seconds))
|
||||
if (u.Max > 0 && src > u.Max) || (u.Min > 0 && src < u.Min) {
|
||||
u.LastCycleStatus[server.ID] = struct{}{}
|
||||
u.LastCycleStatus[server.ID] = false
|
||||
} else {
|
||||
u.LastCycleStatus[server.ID] = nil
|
||||
u.LastCycleStatus[server.ID] = true
|
||||
}
|
||||
if cycleTransferStats.ServerName[server.ID] != server.Name {
|
||||
cycleTransferStats.ServerName[server.ID] = server.Name
|
||||
@ -166,94 +166,94 @@ func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server,
|
||||
}
|
||||
|
||||
if u.Type == "offline" && float64(time.Now().Unix())-src > 6 {
|
||||
return struct{}{}
|
||||
return false
|
||||
} else if (u.Max > 0 && src > u.Max) || (u.Min > 0 && src < u.Min) {
|
||||
return struct{}{}
|
||||
return false
|
||||
}
|
||||
|
||||
return nil
|
||||
return true
|
||||
}
|
||||
|
||||
// IsTransferDurationRule 判断该规则是否属于周期流量规则 属于则返回true
|
||||
func (rule Rule) IsTransferDurationRule() bool {
|
||||
return strings.HasSuffix(rule.Type, "_cycle")
|
||||
func (u *Rule) IsTransferDurationRule() bool {
|
||||
return strings.HasSuffix(u.Type, "_cycle")
|
||||
}
|
||||
|
||||
// GetTransferDurationStart 获取周期流量的起始时间
|
||||
func (rule Rule) GetTransferDurationStart() time.Time {
|
||||
func (u *Rule) GetTransferDurationStart() time.Time {
|
||||
// Accept uppercase and lowercase
|
||||
unit := strings.ToLower(rule.CycleUnit)
|
||||
startTime := *rule.CycleStart
|
||||
unit := strings.ToLower(u.CycleUnit)
|
||||
startTime := *u.CycleStart
|
||||
var nextTime time.Time
|
||||
switch unit {
|
||||
case "year":
|
||||
nextTime = startTime.AddDate(int(rule.CycleInterval), 0, 0)
|
||||
nextTime = startTime.AddDate(int(u.CycleInterval), 0, 0)
|
||||
for time.Now().After(nextTime) {
|
||||
startTime = nextTime
|
||||
nextTime = nextTime.AddDate(int(rule.CycleInterval), 0, 0)
|
||||
nextTime = nextTime.AddDate(int(u.CycleInterval), 0, 0)
|
||||
}
|
||||
case "month":
|
||||
nextTime = startTime.AddDate(0, int(rule.CycleInterval), 0)
|
||||
nextTime = startTime.AddDate(0, int(u.CycleInterval), 0)
|
||||
for time.Now().After(nextTime) {
|
||||
startTime = nextTime
|
||||
nextTime = nextTime.AddDate(0, int(rule.CycleInterval), 0)
|
||||
nextTime = nextTime.AddDate(0, int(u.CycleInterval), 0)
|
||||
}
|
||||
case "week":
|
||||
nextTime = startTime.AddDate(0, 0, 7*int(rule.CycleInterval))
|
||||
nextTime = startTime.AddDate(0, 0, 7*int(u.CycleInterval))
|
||||
for time.Now().After(nextTime) {
|
||||
startTime = nextTime
|
||||
nextTime = nextTime.AddDate(0, 0, 7*int(rule.CycleInterval))
|
||||
nextTime = nextTime.AddDate(0, 0, 7*int(u.CycleInterval))
|
||||
}
|
||||
case "day":
|
||||
nextTime = startTime.AddDate(0, 0, int(rule.CycleInterval))
|
||||
nextTime = startTime.AddDate(0, 0, int(u.CycleInterval))
|
||||
for time.Now().After(nextTime) {
|
||||
startTime = nextTime
|
||||
nextTime = nextTime.AddDate(0, 0, int(rule.CycleInterval))
|
||||
nextTime = nextTime.AddDate(0, 0, int(u.CycleInterval))
|
||||
}
|
||||
default:
|
||||
// For hour unit or not set.
|
||||
interval := 3600 * int64(rule.CycleInterval)
|
||||
startTime = time.Unix(rule.CycleStart.Unix()+(time.Now().Unix()-rule.CycleStart.Unix())/interval*interval, 0)
|
||||
interval := 3600 * int64(u.CycleInterval)
|
||||
startTime = time.Unix(u.CycleStart.Unix()+(time.Now().Unix()-u.CycleStart.Unix())/interval*interval, 0)
|
||||
}
|
||||
|
||||
return startTime
|
||||
}
|
||||
|
||||
// GetTransferDurationEnd 获取周期流量结束时间
|
||||
func (rule Rule) GetTransferDurationEnd() time.Time {
|
||||
func (u *Rule) GetTransferDurationEnd() time.Time {
|
||||
// Accept uppercase and lowercase
|
||||
unit := strings.ToLower(rule.CycleUnit)
|
||||
startTime := *rule.CycleStart
|
||||
unit := strings.ToLower(u.CycleUnit)
|
||||
startTime := *u.CycleStart
|
||||
var nextTime time.Time
|
||||
switch unit {
|
||||
case "year":
|
||||
nextTime = startTime.AddDate(int(rule.CycleInterval), 0, 0)
|
||||
nextTime = startTime.AddDate(int(u.CycleInterval), 0, 0)
|
||||
for time.Now().After(nextTime) {
|
||||
startTime = nextTime
|
||||
nextTime = nextTime.AddDate(int(rule.CycleInterval), 0, 0)
|
||||
nextTime = nextTime.AddDate(int(u.CycleInterval), 0, 0)
|
||||
}
|
||||
case "month":
|
||||
nextTime = startTime.AddDate(0, int(rule.CycleInterval), 0)
|
||||
nextTime = startTime.AddDate(0, int(u.CycleInterval), 0)
|
||||
for time.Now().After(nextTime) {
|
||||
startTime = nextTime
|
||||
nextTime = nextTime.AddDate(0, int(rule.CycleInterval), 0)
|
||||
nextTime = nextTime.AddDate(0, int(u.CycleInterval), 0)
|
||||
}
|
||||
case "week":
|
||||
nextTime = startTime.AddDate(0, 0, 7*int(rule.CycleInterval))
|
||||
nextTime = startTime.AddDate(0, 0, 7*int(u.CycleInterval))
|
||||
for time.Now().After(nextTime) {
|
||||
startTime = nextTime
|
||||
nextTime = nextTime.AddDate(0, 0, 7*int(rule.CycleInterval))
|
||||
nextTime = nextTime.AddDate(0, 0, 7*int(u.CycleInterval))
|
||||
}
|
||||
case "day":
|
||||
nextTime = startTime.AddDate(0, 0, int(rule.CycleInterval))
|
||||
nextTime = startTime.AddDate(0, 0, int(u.CycleInterval))
|
||||
for time.Now().After(nextTime) {
|
||||
startTime = nextTime
|
||||
nextTime = nextTime.AddDate(0, 0, int(rule.CycleInterval))
|
||||
nextTime = nextTime.AddDate(0, 0, int(u.CycleInterval))
|
||||
}
|
||||
default:
|
||||
// For hour unit or not set.
|
||||
interval := 3600 * int64(rule.CycleInterval)
|
||||
startTime = time.Unix(rule.CycleStart.Unix()+(time.Now().Unix()-rule.CycleStart.Unix())/interval*interval, 0)
|
||||
interval := 3600 * int64(u.CycleInterval)
|
||||
startTime = time.Unix(u.CycleStart.Unix()+(time.Now().Unix()-u.CycleStart.Unix())/interval*interval, 0)
|
||||
nextTime = time.Unix(startTime.Unix()+interval, 0)
|
||||
}
|
||||
|
||||
|
@ -1,34 +1,34 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/naiba/nezha/pkg/utils"
|
||||
pb "github.com/naiba/nezha/proto"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/nezhahq/nezha/pkg/utils"
|
||||
pb "github.com/nezhahq/nezha/proto"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
Common
|
||||
Name string
|
||||
Tag string // 分组名
|
||||
Secret string `gorm:"uniqueIndex" json:"-"`
|
||||
Note string `json:"-"` // 管理员可见备注
|
||||
PublicNote string `json:"PublicNote,omitempty"` // 公开备注
|
||||
DisplayIndex int // 展示排序,越大越靠前
|
||||
HideForGuest bool // 对游客隐藏
|
||||
EnableDDNS bool // 启用DDNS
|
||||
DDNSProfiles []uint64 `gorm:"-" json:"-"` // DDNS配置
|
||||
|
||||
Name string `json:"name"`
|
||||
UUID string `json:"uuid,omitempty" gorm:"unique"`
|
||||
Note string `json:"note,omitempty"` // 管理员可见备注
|
||||
PublicNote string `json:"public_note,omitempty"` // 公开备注
|
||||
DisplayIndex int `json:"display_index"` // 展示排序,越大越靠前
|
||||
HideForGuest bool `json:"hide_for_guest,omitempty"` // 对游客隐藏
|
||||
EnableDDNS bool `json:"enable_ddns,omitempty"` // 启用DDNS
|
||||
DDNSProfilesRaw string `gorm:"default:'[]';column:ddns_profiles_raw" json:"-"`
|
||||
|
||||
Host *Host `gorm:"-"`
|
||||
State *HostState `gorm:"-"`
|
||||
LastActive time.Time `gorm:"-"`
|
||||
DDNSProfiles []uint64 `gorm:"-" json:"ddns_profiles,omitempty" validate:"optional"` // DDNS配置
|
||||
|
||||
Host *Host `gorm:"-" json:"host,omitempty"`
|
||||
State *HostState `gorm:"-" json:"state,omitempty"`
|
||||
GeoIP *GeoIP `gorm:"-" json:"geoip,omitempty"`
|
||||
LastActive time.Time `gorm:"-" json:"last_active,omitempty"`
|
||||
|
||||
TaskClose chan error `gorm:"-" json:"-"`
|
||||
TaskCloseLock *sync.Mutex `gorm:"-" json:"-"`
|
||||
@ -41,6 +41,7 @@ type Server struct {
|
||||
func (s *Server) CopyFromRunningServer(old *Server) {
|
||||
s.Host = old.Host
|
||||
s.State = old.State
|
||||
s.GeoIP = old.GeoIP
|
||||
s.LastActive = old.LastActive
|
||||
s.TaskClose = old.TaskClose
|
||||
s.TaskCloseLock = old.TaskCloseLock
|
||||
@ -58,20 +59,3 @@ func (s *Server) AfterFind(tx *gorm.DB) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func boolToString(b bool) string {
|
||||
if b {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
|
||||
func (s Server) MarshalForDashboard() template.JS {
|
||||
name, _ := utils.Json.Marshal(s.Name)
|
||||
tag, _ := utils.Json.Marshal(s.Tag)
|
||||
note, _ := utils.Json.Marshal(s.Note)
|
||||
secret, _ := utils.Json.Marshal(s.Secret)
|
||||
ddnsProfilesRaw, _ := utils.Json.Marshal(s.DDNSProfilesRaw)
|
||||
publicNote, _ := utils.Json.Marshal(s.PublicNote)
|
||||
return template.JS(fmt.Sprintf(`{"ID":%d,"Name":%s,"Secret":%s,"DisplayIndex":%d,"Tag":%s,"Note":%s,"HideForGuest": %s,"EnableDDNS": %s,"DDNSProfilesRaw": %s,"PublicNote": %s}`, s.ID, name, secret, s.DisplayIndex, tag, note, boolToString(s.HideForGuest), boolToString(s.EnableDDNS), ddnsProfilesRaw, publicNote))
|
||||
}
|
||||
|
36
model/server_api.go
Normal file
36
model/server_api.go
Normal file
@ -0,0 +1,36 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type StreamServer struct {
|
||||
ID uint64 `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
PublicNote string `json:"public_note,omitempty"` // 公开备注,只第一个数据包有值
|
||||
DisplayIndex int `json:"display_index,omitempty"` // 展示排序,越大越靠前
|
||||
|
||||
Host *Host `json:"host,omitempty"`
|
||||
State *HostState `json:"state,omitempty"`
|
||||
CountryCode string `json:"country_code,omitempty"`
|
||||
LastActive time.Time `json:"last_active,omitempty"`
|
||||
}
|
||||
|
||||
type StreamServerData struct {
|
||||
Now int64 `json:"now,omitempty"`
|
||||
Servers []StreamServer `json:"servers,omitempty"`
|
||||
}
|
||||
|
||||
type ServerForm struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Note string `json:"note,omitempty" validate:"optional"` // 管理员可见备注
|
||||
PublicNote string `json:"public_note,omitempty" validate:"optional"` // 公开备注
|
||||
DisplayIndex int `json:"display_index,omitempty" default:"0"` // 展示排序,越大越靠前
|
||||
HideForGuest bool `json:"hide_for_guest,omitempty" validate:"optional"` // 对游客隐藏
|
||||
EnableDDNS bool `json:"enable_ddns,omitempty" validate:"optional"` // 启用DDNS
|
||||
DDNSProfiles []uint64 `gorm:"-" json:"ddns_profiles,omitempty" validate:"optional"` // DDNS配置
|
||||
}
|
||||
|
||||
type ForceUpdateResponse struct {
|
||||
Success []uint64 `json:"success,omitempty" validate:"optional"`
|
||||
Failure []uint64 `json:"failure,omitempty" validate:"optional"`
|
||||
Offline []uint64 `json:"offline,omitempty" validate:"optional"`
|
||||
}
|
7
model/server_group.go
Normal file
7
model/server_group.go
Normal file
@ -0,0 +1,7 @@
|
||||
package model
|
||||
|
||||
type ServerGroup struct {
|
||||
Common
|
||||
|
||||
Name string `json:"name"`
|
||||
}
|
11
model/server_group_api.go
Normal file
11
model/server_group_api.go
Normal file
@ -0,0 +1,11 @@
|
||||
package model
|
||||
|
||||
type ServerGroupForm struct {
|
||||
Name string `json:"name" minLength:"1"`
|
||||
Servers []uint64 `json:"servers"`
|
||||
}
|
||||
|
||||
type ServerGroupResponseItem struct {
|
||||
Group ServerGroup `json:"group"`
|
||||
Servers []uint64 `json:"servers"`
|
||||
}
|
7
model/server_group_server.go
Normal file
7
model/server_group_server.go
Normal file
@ -0,0 +1,7 @@
|
||||
package model
|
||||
|
||||
type ServerGroupServer struct {
|
||||
Common
|
||||
ServerGroupId uint64 `json:"server_group_id" gorm:"uniqueIndex:idx_server_group_server"`
|
||||
ServerId uint64 `json:"server_id" gorm:"uniqueIndex:idx_server_group_server"`
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/naiba/nezha/pkg/utils"
|
||||
)
|
||||
|
||||
func TestServerMarshal(t *testing.T) {
|
||||
patterns := []string{
|
||||
"asd > asd",
|
||||
"asd \" asd",
|
||||
"asd } asd",
|
||||
}
|
||||
|
||||
for i := 0; i < len(patterns); i++ {
|
||||
server := Server{
|
||||
Name: patterns[i],
|
||||
Tag: patterns[i],
|
||||
}
|
||||
serverStr := string(server.MarshalForDashboard())
|
||||
var serverRestore Server
|
||||
if utils.Json.Unmarshal([]byte(serverStr), &serverRestore) != nil {
|
||||
t.Fatalf("Error: %s", serverStr)
|
||||
}
|
||||
if server.Name != serverRestore.Name {
|
||||
t.Fatalf("Expected %s, but got %s", server.Name, serverRestore.Name)
|
||||
}
|
||||
}
|
||||
}
|
131
model/service.go
Normal file
131
model/service.go
Normal file
@ -0,0 +1,131 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/nezhahq/nezha/pkg/utils"
|
||||
pb "github.com/nezhahq/nezha/proto"
|
||||
)
|
||||
|
||||
const (
|
||||
_ = iota
|
||||
TaskTypeHTTPGet
|
||||
TaskTypeICMPPing
|
||||
TaskTypeTCPPing
|
||||
TaskTypeCommand
|
||||
TaskTypeTerminal
|
||||
TaskTypeUpgrade
|
||||
TaskTypeKeepalive
|
||||
TaskTypeTerminalGRPC
|
||||
TaskTypeNAT
|
||||
TaskTypeReportHostInfo
|
||||
TaskTypeFM
|
||||
)
|
||||
|
||||
type TerminalTask struct {
|
||||
StreamID string
|
||||
}
|
||||
|
||||
type TaskNAT struct {
|
||||
StreamID string
|
||||
Host string
|
||||
}
|
||||
|
||||
type TaskFM struct {
|
||||
StreamID string
|
||||
}
|
||||
|
||||
const (
|
||||
ServiceCoverAll = iota
|
||||
ServiceCoverIgnoreAll
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
Common
|
||||
Name string `json:"name"`
|
||||
Type uint8 `json:"type"`
|
||||
Target string `json:"target"`
|
||||
SkipServersRaw string `json:"-"`
|
||||
Duration uint64 `json:"duration"`
|
||||
Notify bool `json:"notify,omitempty"`
|
||||
NotificationGroupID uint64 `json:"notification_group_id"` // 当前服务监控所属的通知组 ID
|
||||
Cover uint8 `json:"cover"`
|
||||
|
||||
EnableTriggerTask bool `gorm:"default: false" json:"enable_trigger_task,omitempty"`
|
||||
EnableShowInService bool `gorm:"default: false" json:"enable_show_in_service,omitempty"`
|
||||
FailTriggerTasksRaw string `gorm:"default:'[]'" json:"-"`
|
||||
RecoverTriggerTasksRaw string `gorm:"default:'[]'" json:"-"`
|
||||
|
||||
FailTriggerTasks []uint64 `gorm:"-" json:"fail_trigger_tasks"` // 失败时执行的触发任务id
|
||||
RecoverTriggerTasks []uint64 `gorm:"-" json:"recover_trigger_tasks"` // 恢复时执行的触发任务id
|
||||
|
||||
MinLatency float32 `json:"min_latency"`
|
||||
MaxLatency float32 `json:"max_latency"`
|
||||
LatencyNotify bool `json:"latency_notify,omitempty"`
|
||||
|
||||
SkipServers map[uint64]bool `gorm:"-" json:"skip_servers"`
|
||||
CronJobID cron.EntryID `gorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
func (m *Service) PB() *pb.Task {
|
||||
return &pb.Task{
|
||||
Id: m.ID,
|
||||
Type: uint64(m.Type),
|
||||
Data: m.Target,
|
||||
}
|
||||
}
|
||||
|
||||
// CronSpec 返回服务监控请求间隔对应的 cron 表达式
|
||||
func (m *Service) CronSpec() string {
|
||||
if m.Duration == 0 {
|
||||
// 默认间隔 30 秒
|
||||
m.Duration = 30
|
||||
}
|
||||
return fmt.Sprintf("@every %ds", m.Duration)
|
||||
}
|
||||
|
||||
func (m *Service) BeforeSave(tx *gorm.DB) error {
|
||||
if data, err := utils.Json.Marshal(m.SkipServers); err != nil {
|
||||
return err
|
||||
} else {
|
||||
m.SkipServersRaw = string(data)
|
||||
}
|
||||
if data, err := utils.Json.Marshal(m.FailTriggerTasks); err != nil {
|
||||
return err
|
||||
} else {
|
||||
m.FailTriggerTasksRaw = string(data)
|
||||
}
|
||||
if data, err := utils.Json.Marshal(m.RecoverTriggerTasks); err != nil {
|
||||
return err
|
||||
} else {
|
||||
m.RecoverTriggerTasksRaw = string(data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Service) AfterFind(tx *gorm.DB) error {
|
||||
m.SkipServers = make(map[uint64]bool)
|
||||
if err := utils.Json.Unmarshal([]byte(m.SkipServersRaw), &m.SkipServers); err != nil {
|
||||
log.Println("NEZHA>> Service.AfterFind:", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 加载触发任务列表
|
||||
if err := utils.Json.Unmarshal([]byte(m.FailTriggerTasksRaw), &m.FailTriggerTasks); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := utils.Json.Unmarshal([]byte(m.RecoverTriggerTasksRaw), &m.RecoverTriggerTasks); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsServiceSentinelNeeded 判断该任务类型是否需要进行服务监控 需要则返回true
|
||||
func IsServiceSentinelNeeded(t uint64) bool {
|
||||
return t != TaskTypeCommand && t != TaskTypeTerminalGRPC && t != TaskTypeUpgrade
|
||||
}
|
55
model/service_api.go
Normal file
55
model/service_api.go
Normal file
@ -0,0 +1,55 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type ServiceForm struct {
|
||||
Name string `json:"name,omitempty" minLength:"1"`
|
||||
Target string `json:"target,omitempty"`
|
||||
Type uint8 `json:"type,omitempty"`
|
||||
Cover uint8 `json:"cover,omitempty"`
|
||||
Notify bool `json:"notify,omitempty" validate:"optional"`
|
||||
Duration uint64 `json:"duration,omitempty"`
|
||||
MinLatency float32 `json:"min_latency,omitempty" default:"0.0"`
|
||||
MaxLatency float32 `json:"max_latency,omitempty" default:"0.0"`
|
||||
LatencyNotify bool `json:"latency_notify,omitempty" validate:"optional"`
|
||||
EnableTriggerTask bool `json:"enable_trigger_task,omitempty" validate:"optional"`
|
||||
EnableShowInService bool `json:"enable_show_in_service,omitempty" validate:"optional"`
|
||||
FailTriggerTasks []uint64 `json:"fail_trigger_tasks,omitempty"`
|
||||
RecoverTriggerTasks []uint64 `json:"recover_trigger_tasks,omitempty"`
|
||||
SkipServers map[uint64]bool `json:"skip_servers,omitempty"`
|
||||
NotificationGroupID uint64 `json:"notification_group_id,omitempty"`
|
||||
}
|
||||
|
||||
type ServiceResponseItem struct {
|
||||
Service *Service `json:"service,omitempty"`
|
||||
CurrentUp uint64 `json:"current_up"`
|
||||
CurrentDown uint64 `json:"current_down"`
|
||||
TotalUp uint64 `json:"total_up"`
|
||||
TotalDown uint64 `json:"total_down"`
|
||||
Delay *[30]float32 `json:"delay,omitempty"`
|
||||
Up *[30]int `json:"up,omitempty"`
|
||||
Down *[30]int `json:"down,omitempty"`
|
||||
}
|
||||
|
||||
func (r ServiceResponseItem) TotalUptime() float32 {
|
||||
if r.TotalUp+r.TotalDown == 0 {
|
||||
return 0
|
||||
}
|
||||
return float32(r.TotalUp) / (float32(r.TotalUp + r.TotalDown)) * 100
|
||||
}
|
||||
|
||||
type CycleTransferStats struct {
|
||||
Name string `json:"name"`
|
||||
From time.Time `json:"from"`
|
||||
To time.Time `json:"to"`
|
||||
Max uint64 `json:"max"`
|
||||
Min uint64 `json:"min"`
|
||||
ServerName map[uint64]string `json:"server_name,omitempty"`
|
||||
Transfer map[uint64]uint64 `json:"transfer,omitempty"`
|
||||
NextUpdate map[uint64]time.Time `json:"next_update,omitempty"`
|
||||
}
|
||||
|
||||
type ServiceResponse struct {
|
||||
Services map[uint64]ServiceResponseItem `json:"services,omitempty"`
|
||||
CycleTransferStats map[uint64]CycleTransferStats `json:"cycle_transfer_stats,omitempty"`
|
||||
}
|
17
model/service_history.go
Normal file
17
model/service_history.go
Normal file
@ -0,0 +1,17 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type ServiceHistory struct {
|
||||
ID uint64 `gorm:"primaryKey" json:"id,omitempty"`
|
||||
CreatedAt time.Time `gorm:"index;<-:create;index:idx_server_id_created_at_service_id_avg_delay" json:"created_at,omitempty"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at,omitempty"`
|
||||
ServiceID uint64 `gorm:"index:idx_server_id_created_at_service_id_avg_delay" json:"service_id,omitempty"`
|
||||
ServerID uint64 `gorm:"index:idx_server_id_created_at_service_id_avg_delay" json:"server_id,omitempty"`
|
||||
AvgDelay float32 `gorm:"index:idx_server_id_created_at_service_id_avg_delay" json:"avg_delay,omitempty"` // 平均延迟,毫秒
|
||||
Up uint64 `json:"up,omitempty"` // 检查状态良好计数
|
||||
Down uint64 `json:"down,omitempty"` // 检查状态异常计数
|
||||
Data string `json:"data,omitempty"`
|
||||
}
|
10
model/service_history_api.go
Normal file
10
model/service_history_api.go
Normal file
@ -0,0 +1,10 @@
|
||||
package model
|
||||
|
||||
type ServiceInfos struct {
|
||||
ServiceID uint64 `json:"monitor_id"`
|
||||
ServerID uint64 `json:"server_id"`
|
||||
ServiceName string `json:"monitor_name"`
|
||||
ServerName string `json:"server_name"`
|
||||
CreatedAt []int64 `json:"created_at"`
|
||||
AvgDelay []float32 `json:"avg_delay"`
|
||||
}
|
17
model/setting_api.go
Normal file
17
model/setting_api.go
Normal file
@ -0,0 +1,17 @@
|
||||
package model
|
||||
|
||||
type SettingForm struct {
|
||||
CustomNameservers string `json:"custom_nameservers,omitempty" validate:"optional"`
|
||||
IgnoredIPNotification string `json:"ignored_ip_notification,omitempty" validate:"optional"`
|
||||
IPChangeNotificationGroupID uint64 `json:"ip_change_notification_group_id,omitempty"` // IP变更提醒的通知组
|
||||
Cover uint8 `json:"cover,omitempty"`
|
||||
SiteName string `json:"site_name,omitempty" minLength:"1"`
|
||||
Language string `json:"language,omitempty" minLength:"2"`
|
||||
InstallHost string `json:"install_host,omitempty" validate:"optional"`
|
||||
CustomCode string `json:"custom_code,omitempty" validate:"optional"`
|
||||
CustomCodeDashboard string `json:"custom_code_dashboard,omitempty" validate:"optional"`
|
||||
RealIPHeader string `json:"real_ip_header,omitempty" validate:"optional"` // 真实IP
|
||||
|
||||
EnableIPChangeNotification bool `json:"enable_ip_change_notification,omitempty" validate:"optional"`
|
||||
EnablePlainIPInNotification bool `json:"enable_plain_ip_in_notification,omitempty" validate:"optional"`
|
||||
}
|
12
model/terminal_api.go
Normal file
12
model/terminal_api.go
Normal file
@ -0,0 +1,12 @@
|
||||
package model
|
||||
|
||||
type TerminalForm struct {
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
ServerID uint64 `json:"server_id,omitempty"`
|
||||
}
|
||||
|
||||
type CreateTerminalResponse struct {
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
ServerID uint64 `json:"server_id,omitempty"`
|
||||
ServerName string `json:"server_name,omitempty"`
|
||||
}
|
@ -2,7 +2,7 @@ package model
|
||||
|
||||
type Transfer struct {
|
||||
Common
|
||||
ServerID uint64
|
||||
In uint64
|
||||
Out uint64
|
||||
ServerID uint64 `json:"server_id"`
|
||||
In uint64 `json:"in"`
|
||||
Out uint64 `json:"out"`
|
||||
}
|
||||
|
@ -1,71 +1,12 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"github.com/google/go-github/v47/github"
|
||||
"github.com/xanzy/go-gitlab"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Common
|
||||
Login string `json:"login,omitempty"` // 登录名
|
||||
AvatarURL string `json:"avatar_url,omitempty"` // 头像地址
|
||||
Name string `json:"name,omitempty"` // 昵称
|
||||
Blog string `json:"blog,omitempty"` // 网站链接
|
||||
Email string `json:"email,omitempty"` // 邮箱
|
||||
Hireable bool `json:"hireable,omitempty"`
|
||||
Bio string `json:"bio,omitempty"` // 个人简介
|
||||
|
||||
Token string `json:"-"` // 认证 Token
|
||||
TokenExpired time.Time `json:"token_expired,omitempty"` // Token 过期时间
|
||||
SuperAdmin bool `json:"super_admin,omitempty"` // 超级管理员
|
||||
Username string `json:"username,omitempty" gorm:"uniqueIndex"`
|
||||
Password string `json:"password,omitempty" gorm:"type:char(72)"`
|
||||
}
|
||||
|
||||
func NewUserFromGitea(gu *gitea.User) User {
|
||||
var u User
|
||||
u.ID = uint64(gu.ID)
|
||||
u.Login = gu.UserName
|
||||
u.AvatarURL = gu.AvatarURL
|
||||
u.Name = gu.FullName
|
||||
if u.Name == "" {
|
||||
u.Name = u.Login
|
||||
}
|
||||
u.Blog = gu.Website
|
||||
u.Email = gu.Email
|
||||
u.Bio = gu.Description
|
||||
return u
|
||||
}
|
||||
|
||||
func NewUserFromGitlab(gu *gitlab.User) User {
|
||||
var u User
|
||||
u.ID = uint64(gu.ID)
|
||||
u.Login = gu.Username
|
||||
u.AvatarURL = gu.AvatarURL
|
||||
u.Name = gu.Name
|
||||
if u.Name == "" {
|
||||
u.Name = u.Login
|
||||
}
|
||||
u.Blog = gu.WebsiteURL
|
||||
u.Email = gu.Email
|
||||
u.Bio = gu.Bio
|
||||
return u
|
||||
}
|
||||
|
||||
func NewUserFromGitHub(gu *github.User) User {
|
||||
var u User
|
||||
u.ID = uint64(gu.GetID())
|
||||
u.Login = gu.GetLogin()
|
||||
u.AvatarURL = gu.GetAvatarURL()
|
||||
u.Name = gu.GetName()
|
||||
// 昵称为空的情况
|
||||
if u.Name == "" {
|
||||
u.Name = u.Login
|
||||
}
|
||||
u.Blog = gu.GetBlog()
|
||||
u.Email = gu.GetEmail()
|
||||
u.Hireable = gu.GetHireable()
|
||||
u.Bio = gu.GetBio()
|
||||
return u
|
||||
type Profile struct {
|
||||
User
|
||||
LoginIP string `json:"login_ip,omitempty"`
|
||||
}
|
||||
|
12
model/user_api.go
Normal file
12
model/user_api.go
Normal file
@ -0,0 +1,12 @@
|
||||
package model
|
||||
|
||||
type UserForm struct {
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty" gorm:"type:char(72)"`
|
||||
}
|
||||
|
||||
type ProfileForm struct {
|
||||
OriginalPassword string `json:"original_password,omitempty"`
|
||||
NewUsername string `json:"new_username,omitempty"`
|
||||
NewPassword string `json:"new_password,omitempty"`
|
||||
}
|
6
model/user_group.go
Normal file
6
model/user_group.go
Normal file
@ -0,0 +1,6 @@
|
||||
package model
|
||||
|
||||
type UserGroup struct {
|
||||
Common
|
||||
Name string `json:"name"`
|
||||
}
|
7
model/user_group_user.go
Normal file
7
model/user_group_user.go
Normal file
@ -0,0 +1,7 @@
|
||||
package model
|
||||
|
||||
type UserGroupUser struct {
|
||||
Common
|
||||
UserGroupId uint64 `json:"user_group_id"`
|
||||
UserId uint64 `json:"user_id"`
|
||||
}
|
116
model/waf.go
Normal file
116
model/waf.go
Normal file
@ -0,0 +1,116 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/nezhahq/nezha/pkg/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
_ uint8 = iota
|
||||
WAFBlockReasonTypeLoginFail
|
||||
WAFBlockReasonTypeBruteForceToken
|
||||
WAFBlockReasonTypeAgentAuthFail
|
||||
)
|
||||
|
||||
type WAFApiMock struct {
|
||||
IP string `json:"ip,omitempty"`
|
||||
Count uint64 `json:"count,omitempty"`
|
||||
LastBlockReason uint8 `json:"last_block_reason,omitempty"`
|
||||
LastBlockTimestamp uint64 `json:"last_block_timestamp,omitempty"`
|
||||
}
|
||||
|
||||
type WAF struct {
|
||||
IP []byte `gorm:"type:binary(16);primaryKey" json:"ip,omitempty"`
|
||||
Count uint64 `json:"count,omitempty"`
|
||||
LastBlockReason uint8 `json:"last_block_reason,omitempty"`
|
||||
LastBlockTimestamp uint64 `json:"last_block_timestamp,omitempty"`
|
||||
}
|
||||
|
||||
func (w *WAF) TableName() string {
|
||||
return "waf"
|
||||
}
|
||||
|
||||
func CheckIP(db *gorm.DB, ip string) error {
|
||||
if ip == "" {
|
||||
return nil
|
||||
}
|
||||
ipBinary, err := utils.IPStringToBinary(ip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var w WAF
|
||||
if err := db.First(&w, "ip = ?", ipBinary).Error; err != nil {
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
return err
|
||||
}
|
||||
}
|
||||
now := time.Now().Unix()
|
||||
if powAdd(w.Count, 4, w.LastBlockTimestamp) > uint64(now) {
|
||||
return errors.New("you are blocked by nezha WAF")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ClearIP(db *gorm.DB, ip string) error {
|
||||
if ip == "" {
|
||||
return nil
|
||||
}
|
||||
ipBinary, err := utils.IPStringToBinary(ip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Unscoped().Delete(&WAF{}, "ip = ?", ipBinary).Error
|
||||
}
|
||||
|
||||
func BatchClearIP(db *gorm.DB, ip []string) error {
|
||||
if len(ip) < 1 {
|
||||
return nil
|
||||
}
|
||||
ips := make([][]byte, 0, len(ip))
|
||||
for _, s := range ip {
|
||||
ipBinary, err := utils.IPStringToBinary(s)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ips = append(ips, ipBinary)
|
||||
}
|
||||
return db.Unscoped().Delete(&WAF{}, "ip in (?)", ips).Error
|
||||
}
|
||||
|
||||
func BlockIP(db *gorm.DB, ip string, reason uint8) error {
|
||||
if ip == "" {
|
||||
return nil
|
||||
}
|
||||
ipBinary, err := utils.IPStringToBinary(ip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var w WAF
|
||||
w.IP = ipBinary
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where(&w).Attrs(WAF{
|
||||
LastBlockReason: reason,
|
||||
LastBlockTimestamp: uint64(time.Now().Unix()),
|
||||
}).FirstOrCreate(&w).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Exec("UPDATE waf SET count = count + 1, last_block_reason = ?, last_block_timestamp = ? WHERE ip = ?", reason, uint64(time.Now().Unix()), ipBinary).Error
|
||||
})
|
||||
}
|
||||
|
||||
func powAdd(x, y, z uint64) uint64 {
|
||||
base := big.NewInt(0).SetUint64(x)
|
||||
exp := big.NewInt(0).SetUint64(y)
|
||||
result := big.NewInt(1)
|
||||
result.Exp(base, exp, nil)
|
||||
result.Add(result, big.NewInt(0).SetUint64(z))
|
||||
if !result.IsUint64() {
|
||||
return ^uint64(0) // return max uint64 value on overflow
|
||||
}
|
||||
ret := result.Uint64()
|
||||
return utils.IfOr(ret < z+3, z+3, ret)
|
||||
}
|
@ -10,8 +10,8 @@ import (
|
||||
"github.com/libdns/libdns"
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/naiba/nezha/model"
|
||||
"github.com/naiba/nezha/pkg/utils"
|
||||
"github.com/nezhahq/nezha/model"
|
||||
"github.com/nezhahq/nezha/pkg/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -9,8 +9,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/libdns/libdns"
|
||||
"github.com/naiba/nezha/model"
|
||||
"github.com/naiba/nezha/pkg/utils"
|
||||
"github.com/nezhahq/nezha/model"
|
||||
"github.com/nezhahq/nezha/pkg/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/naiba/nezha/model"
|
||||
"github.com/nezhahq/nezha/model"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -1,21 +1,26 @@
|
||||
package geoip
|
||||
|
||||
import (
|
||||
"embed"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
maxminddb "github.com/oschwald/maxminddb-golang"
|
||||
)
|
||||
|
||||
//go:embed geoip.db
|
||||
var geoDBFS embed.FS
|
||||
var db []byte
|
||||
|
||||
var (
|
||||
dbData []byte
|
||||
err error
|
||||
dbOnce = sync.OnceValues(func() (*maxminddb.Reader, error) {
|
||||
db, err := maxminddb.FromBytes(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return db, nil
|
||||
})
|
||||
)
|
||||
|
||||
type IPInfo struct {
|
||||
@ -25,21 +30,14 @@ type IPInfo struct {
|
||||
ContinentName string `maxminddb:"continent_name"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
dbData, err = geoDBFS.ReadFile("geoip.db")
|
||||
if err != nil {
|
||||
log.Printf("NEZHA>> Failed to open geoip database: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func Lookup(ip net.IP, record *IPInfo) (string, error) {
|
||||
db, err := maxminddb.FromBytes(dbData)
|
||||
func Lookup(ip net.IP) (string, error) {
|
||||
db, err := dbOnce()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = db.Lookup(ip, record)
|
||||
var record IPInfo
|
||||
err = db.Lookup(ip, &record)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -5,10 +5,10 @@ import (
|
||||
"io"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/naiba/nezha/proto"
|
||||
"github.com/nezhahq/nezha/proto"
|
||||
)
|
||||
|
||||
var _ io.ReadWriteCloser = &IOStreamWrapper{}
|
||||
var _ io.ReadWriteCloser = (*IOStreamWrapper)(nil)
|
||||
|
||||
type IOStream interface {
|
||||
Recv() (*proto.IOStreamData, error)
|
||||
|
105
pkg/i18n/i18n.go
Normal file
105
pkg/i18n/i18n.go
Normal file
@ -0,0 +1,105 @@
|
||||
package i18n
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/chai2010/gettext-go"
|
||||
)
|
||||
|
||||
//go:embed translations
|
||||
var Translations embed.FS
|
||||
|
||||
var Languages = map[string]string{
|
||||
"zh_CN": "简体中文",
|
||||
"zh_TW": "繁體中文",
|
||||
"en_US": "English",
|
||||
"es_ES": "Español",
|
||||
}
|
||||
|
||||
type Localizer struct {
|
||||
intlMap map[string]gettext.Gettexter
|
||||
lang string
|
||||
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewLocalizer(lang, domain, path string, data any) *Localizer {
|
||||
intl := gettext.New(domain, path, data)
|
||||
intl.SetLanguage(lang)
|
||||
|
||||
intlMap := make(map[string]gettext.Gettexter)
|
||||
intlMap[lang] = intl
|
||||
|
||||
return &Localizer{intlMap: intlMap, lang: lang}
|
||||
}
|
||||
|
||||
func (l *Localizer) SetLanguage(lang string) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
l.lang = lang
|
||||
}
|
||||
|
||||
func (l *Localizer) Exists(lang string) bool {
|
||||
l.mu.RLock()
|
||||
defer l.mu.RUnlock()
|
||||
|
||||
if _, ok := l.intlMap[lang]; ok {
|
||||
return ok
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (l *Localizer) AppendIntl(lang, domain, path string, data any) {
|
||||
intl := gettext.New(domain, path, data)
|
||||
intl.SetLanguage(lang)
|
||||
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
l.intlMap[lang] = intl
|
||||
}
|
||||
|
||||
// Modified from k8s.io/kubectl/pkg/util/i18n
|
||||
|
||||
func (l *Localizer) T(orig string) string {
|
||||
l.mu.RLock()
|
||||
intl, ok := l.intlMap[l.lang]
|
||||
l.mu.RUnlock()
|
||||
if !ok {
|
||||
return orig
|
||||
}
|
||||
|
||||
return intl.PGettext("", orig)
|
||||
}
|
||||
|
||||
// N translates a string, possibly substituting arguments into it along
|
||||
// the way. If len(args) is > 0, args1 is assumed to be the plural value
|
||||
// and plural translation is used.
|
||||
func (l *Localizer) N(orig string, args ...int) string {
|
||||
l.mu.RLock()
|
||||
intl, ok := l.intlMap[l.lang]
|
||||
l.mu.RUnlock()
|
||||
if !ok {
|
||||
return orig
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
return intl.PGettext("", orig)
|
||||
}
|
||||
return fmt.Sprintf(intl.PNGettext("", orig, orig+".plural", args[0]),
|
||||
args[0])
|
||||
}
|
||||
|
||||
// ErrorT produces an error with a translated error string.
|
||||
// Substitution is performed via the `T` function above, following
|
||||
// the same rules.
|
||||
func (l *Localizer) ErrorT(defaultValue string, args ...any) error {
|
||||
return fmt.Errorf(l.T(defaultValue), args...)
|
||||
}
|
||||
|
||||
func (l *Localizer) Tf(defaultValue string, args ...any) string {
|
||||
return fmt.Sprintf(l.T(defaultValue), args...)
|
||||
}
|
220
pkg/i18n/template.pot
Normal file
220
pkg/i18n/template.pot
Normal file
@ -0,0 +1,220 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-11-23 23:56+0800\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=CHARSET\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: cmd/dashboard/controller/alertrule.go:100
|
||||
#, c-format
|
||||
msgid "alert id %d does not exist"
|
||||
msgstr ""
|
||||
|
||||
#: cmd/dashboard/controller/alertrule.go:155
|
||||
msgid "duration need to be at least 3"
|
||||
msgstr ""
|
||||
|
||||
#: cmd/dashboard/controller/alertrule.go:159
|
||||
msgid "cycle_interval need to be at least 1"
|
||||
msgstr ""
|
||||
|
||||
#: cmd/dashboard/controller/alertrule.go:162
|
||||
msgid "cycle_start is not set"
|
||||
msgstr ""
|
||||
|
||||
#: cmd/dashboard/controller/alertrule.go:165
|
||||
msgid "cycle_start is a future value"
|
||||
msgstr ""
|
||||
|
||||
#: cmd/dashboard/controller/alertrule.go:170
|
||||
msgid "need to configure at least a single rule"
|
||||
msgstr ""
|
||||
|
||||
#: cmd/dashboard/controller/controller.go:195
|
||||
msgid "database error"
|
||||
msgstr ""
|
||||
|
||||
#: cmd/dashboard/controller/cron.go:63 cmd/dashboard/controller/cron.go:122
|
||||
msgid "scheduled tasks cannot be triggered by alarms"
|
||||
msgstr ""
|
||||
|
||||
#: cmd/dashboard/controller/cron.go:161
|
||||
#, c-format
|
||||
msgid "task id %d does not exist"
|
||||
msgstr ""
|
||||
|
||||
#: cmd/dashboard/controller/ddns.go:56 cmd/dashboard/controller/ddns.go:120
|
||||
msgid "the retry count must be an integer between 1 and 10"
|
||||
msgstr ""
|
||||
|
||||
#: cmd/dashboard/controller/ddns.go:79 cmd/dashboard/controller/ddns.go:148
|
||||
msgid "error parsing %s: %v"
|
||||
msgstr ""
|
||||
|
||||
#: cmd/dashboard/controller/ddns.go:125 cmd/dashboard/controller/nat.go:95
|
||||
#, c-format
|
||||
msgid "profile id %d does not exist"
|
||||
msgstr ""
|
||||
|
||||
#: cmd/dashboard/controller/fm.go:45 cmd/dashboard/controller/terminal.go:43
|
||||
msgid "server not found or not connected"
|
||||
msgstr ""
|
||||
|
||||
#: cmd/dashboard/controller/notification.go:67
|
||||
#: cmd/dashboard/controller/notification.go:125
|
||||
msgid "a test message"
|
||||
msgstr ""
|
||||
|
||||
#: cmd/dashboard/controller/notification.go:106
|
||||
#, c-format
|
||||
msgid "notification id %d does not exist"
|
||||
msgstr ""
|
||||
|
||||
#: cmd/dashboard/controller/notification_group.go:80
|
||||
#: cmd/dashboard/controller/notification_group.go:142
|
||||
msgid "have invalid notification id"
|
||||
msgstr ""
|
||||
|
||||
#: cmd/dashboard/controller/notification_group.go:131
|
||||
#: cmd/dashboard/controller/server_group.go:130
|
||||
#, c-format
|
||||
msgid "group id %d does not exist"
|
||||
msgstr ""
|
||||
|
||||
#: cmd/dashboard/controller/server.go:60
|
||||
#, c-format
|
||||
msgid "server id %d does not exist"
|
||||
msgstr ""
|
||||
|
||||
#: cmd/dashboard/controller/server_group.go:78
|
||||
#: cmd/dashboard/controller/server_group.go:139
|
||||
msgid "have invalid server id"
|
||||
msgstr ""
|
||||
|
||||
#: cmd/dashboard/controller/service.go:79
|
||||
#: cmd/dashboard/controller/service.go:155
|
||||
msgid "server not found"
|
||||
msgstr ""
|
||||
|
||||
#: cmd/dashboard/controller/service.go:86 cmd/dashboard/controller/user.go:23
|
||||
msgid "unauthorized"
|
||||
msgstr ""
|
||||
|
||||
#: cmd/dashboard/controller/service.go:247
|
||||
#, c-format
|
||||
msgid "service id %d does not exist"
|
||||
msgstr ""
|
||||
|
||||
#: cmd/dashboard/controller/user.go:66
|
||||
msgid "password length must be greater than 6"
|
||||
msgstr ""
|
||||
|
||||
#: cmd/dashboard/controller/user.go:69
|
||||
msgid "username can't be empty"
|
||||
msgstr ""
|
||||
|
||||
#: service/rpc/io_stream.go:122
|
||||
msgid "timeout: no connection established"
|
||||
msgstr ""
|
||||
|
||||
#: service/rpc/io_stream.go:125
|
||||
msgid "timeout: user connection not established"
|
||||
msgstr ""
|
||||
|
||||
#: service/rpc/io_stream.go:128
|
||||
msgid "timeout: agent connection not established"
|
||||
msgstr ""
|
||||
|
||||
#: service/rpc/nezha.go:58
|
||||
msgid "Scheduled Task Executed Successfully"
|
||||
msgstr ""
|
||||
|
||||
#: service/rpc/nezha.go:62
|
||||
msgid "Scheduled Task Executed Failed"
|
||||
msgstr ""
|
||||
|
||||
#: service/rpc/nezha.go:217
|
||||
msgid "IP Changed"
|
||||
msgstr ""
|
||||
|
||||
#: service/singleton/alertsentinel.go:159
|
||||
msgid "Incident"
|
||||
msgstr ""
|
||||
|
||||
#: service/singleton/alertsentinel.go:169
|
||||
msgid "Resolved"
|
||||
msgstr ""
|
||||
|
||||
#: service/singleton/crontask.go:53
|
||||
msgid "Tasks failed to register: ["
|
||||
msgstr ""
|
||||
|
||||
#: service/singleton/crontask.go:60
|
||||
msgid ""
|
||||
"] These tasks will not execute properly. Fix them in the admin dashboard."
|
||||
msgstr ""
|
||||
|
||||
#: service/singleton/crontask.go:146 service/singleton/crontask.go:171
|
||||
#, c-format
|
||||
msgid "[Task failed] %s: server %s is offline and cannot execute the task"
|
||||
msgstr ""
|
||||
|
||||
#: service/singleton/servicesentinel.go:439
|
||||
#, c-format
|
||||
msgid "[Latency] %s %2f > %2f, Reporter: %s"
|
||||
msgstr ""
|
||||
|
||||
#: service/singleton/servicesentinel.go:446
|
||||
#, c-format
|
||||
msgid "[Latency] %s %2f < %2f, Reporter: %s"
|
||||
msgstr ""
|
||||
|
||||
#: service/singleton/servicesentinel.go:472
|
||||
#, c-format
|
||||
msgid "[%s] %s Reporter: %s, Error: %s"
|
||||
msgstr ""
|
||||
|
||||
#: service/singleton/servicesentinel.go:515
|
||||
#, c-format
|
||||
msgid "[TLS] Fetch cert info failed, Reporter: %s, Error: %s"
|
||||
msgstr ""
|
||||
|
||||
#: service/singleton/servicesentinel.go:555
|
||||
#, c-format
|
||||
msgid "The TLS certificate will expire within seven days. Expiration time: %s"
|
||||
msgstr ""
|
||||
|
||||
#: service/singleton/servicesentinel.go:568
|
||||
#, c-format
|
||||
msgid ""
|
||||
"TLS certificate changed, old: issuer %s, expires at %s; new: issuer %s, "
|
||||
"expires at %s"
|
||||
msgstr ""
|
||||
|
||||
#: service/singleton/servicesentinel.go:604
|
||||
msgid "No Data"
|
||||
msgstr ""
|
||||
|
||||
#: service/singleton/servicesentinel.go:606
|
||||
msgid "Good"
|
||||
msgstr ""
|
||||
|
||||
#: service/singleton/servicesentinel.go:608
|
||||
msgid "Low Availability"
|
||||
msgstr ""
|
||||
|
||||
#: service/singleton/servicesentinel.go:610
|
||||
msgid "Down"
|
||||
msgstr ""
|
BIN
pkg/i18n/translations/en_US/LC_MESSAGES/nezha.mo
Normal file
BIN
pkg/i18n/translations/en_US/LC_MESSAGES/nezha.mo
Normal file
Binary file not shown.
224
pkg/i18n/translations/en_US/LC_MESSAGES/nezha.po
Normal file
224
pkg/i18n/translations/en_US/LC_MESSAGES/nezha.po
Normal file
@ -0,0 +1,224 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-11-23 23:56+0800\n"
|
||||
"PO-Revision-Date: 2024-11-01 13:20+0800\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Language: en_US\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 3.5\n"
|
||||
|
||||
#: cmd/dashboard/controller/alertrule.go:100
|
||||
#, c-format
|
||||
msgid "alert id %d does not exist"
|
||||
msgstr "alert id %d does not exist"
|
||||
|
||||
#: cmd/dashboard/controller/alertrule.go:155
|
||||
msgid "duration need to be at least 3"
|
||||
msgstr "duration need to be at least 3"
|
||||
|
||||
#: cmd/dashboard/controller/alertrule.go:159
|
||||
msgid "cycle_interval need to be at least 1"
|
||||
msgstr "cycle_interval need to be at least 1"
|
||||
|
||||
#: cmd/dashboard/controller/alertrule.go:162
|
||||
msgid "cycle_start is not set"
|
||||
msgstr "cycle_start is not set"
|
||||
|
||||
#: cmd/dashboard/controller/alertrule.go:165
|
||||
msgid "cycle_start is a future value"
|
||||
msgstr "cycle_start is a future value"
|
||||
|
||||
#: cmd/dashboard/controller/alertrule.go:170
|
||||
msgid "need to configure at least a single rule"
|
||||
msgstr "need to configure at least a single rule"
|
||||
|
||||
#: cmd/dashboard/controller/controller.go:195
|
||||
msgid "database error"
|
||||
msgstr "database error"
|
||||
|
||||
#: cmd/dashboard/controller/cron.go:63 cmd/dashboard/controller/cron.go:122
|
||||
msgid "scheduled tasks cannot be triggered by alarms"
|
||||
msgstr "scheduled tasks cannot be triggered by alarms"
|
||||
|
||||
#: cmd/dashboard/controller/cron.go:161
|
||||
#, c-format
|
||||
msgid "task id %d does not exist"
|
||||
msgstr "task id %d does not exist"
|
||||
|
||||
#: cmd/dashboard/controller/ddns.go:56 cmd/dashboard/controller/ddns.go:120
|
||||
msgid "the retry count must be an integer between 1 and 10"
|
||||
msgstr "the retry count must be an integer between 1 and 10"
|
||||
|
||||
#: cmd/dashboard/controller/ddns.go:79 cmd/dashboard/controller/ddns.go:148
|
||||
msgid "error parsing %s: %v"
|
||||
msgstr "error parsing %s: %v"
|
||||
|
||||
#: cmd/dashboard/controller/ddns.go:125 cmd/dashboard/controller/nat.go:95
|
||||
#, c-format
|
||||
msgid "profile id %d does not exist"
|
||||
msgstr "profile id %d does not exist"
|
||||
|
||||
#: cmd/dashboard/controller/fm.go:45 cmd/dashboard/controller/terminal.go:43
|
||||
msgid "server not found or not connected"
|
||||
msgstr "server not found or not connected"
|
||||
|
||||
#: cmd/dashboard/controller/notification.go:67
|
||||
#: cmd/dashboard/controller/notification.go:125
|
||||
msgid "a test message"
|
||||
msgstr "a test message"
|
||||
|
||||
#: cmd/dashboard/controller/notification.go:106
|
||||
#, c-format
|
||||
msgid "notification id %d does not exist"
|
||||
msgstr "notification id %d does not exist"
|
||||
|
||||
#: cmd/dashboard/controller/notification_group.go:80
|
||||
#: cmd/dashboard/controller/notification_group.go:142
|
||||
msgid "have invalid notification id"
|
||||
msgstr "have invalid notification id"
|
||||
|
||||
#: cmd/dashboard/controller/notification_group.go:131
|
||||
#: cmd/dashboard/controller/server_group.go:130
|
||||
#, c-format
|
||||
msgid "group id %d does not exist"
|
||||
msgstr "group id %d does not exist"
|
||||
|
||||
#: cmd/dashboard/controller/server.go:60
|
||||
#, c-format
|
||||
msgid "server id %d does not exist"
|
||||
msgstr "server id %d does not exist"
|
||||
|
||||
#: cmd/dashboard/controller/server_group.go:78
|
||||
#: cmd/dashboard/controller/server_group.go:139
|
||||
msgid "have invalid server id"
|
||||
msgstr "have invalid server id"
|
||||
|
||||
#: cmd/dashboard/controller/service.go:79
|
||||
#: cmd/dashboard/controller/service.go:155
|
||||
msgid "server not found"
|
||||
msgstr "server not found"
|
||||
|
||||
#: cmd/dashboard/controller/service.go:86 cmd/dashboard/controller/user.go:23
|
||||
msgid "unauthorized"
|
||||
msgstr "unauthorized"
|
||||
|
||||
#: cmd/dashboard/controller/service.go:247
|
||||
#, c-format
|
||||
msgid "service id %d does not exist"
|
||||
msgstr "service id %d does not exist"
|
||||
|
||||
#: cmd/dashboard/controller/user.go:66
|
||||
msgid "password length must be greater than 6"
|
||||
msgstr "password length must be greater than 6"
|
||||
|
||||
#: cmd/dashboard/controller/user.go:69
|
||||
msgid "username can't be empty"
|
||||
msgstr "username can't be empty"
|
||||
|
||||
#: service/rpc/io_stream.go:122
|
||||
msgid "timeout: no connection established"
|
||||
msgstr "timeout: no connection established"
|
||||
|
||||
#: service/rpc/io_stream.go:125
|
||||
msgid "timeout: user connection not established"
|
||||
msgstr "timeout: user connection not established"
|
||||
|
||||
#: service/rpc/io_stream.go:128
|
||||
msgid "timeout: agent connection not established"
|
||||
msgstr "timeout: agent connection not established"
|
||||
|
||||
#: service/rpc/nezha.go:58
|
||||
msgid "Scheduled Task Executed Successfully"
|
||||
msgstr "Scheduled Task Executed Successfully"
|
||||
|
||||
#: service/rpc/nezha.go:62
|
||||
msgid "Scheduled Task Executed Failed"
|
||||
msgstr "Scheduled Task Executed Failed"
|
||||
|
||||
#: service/rpc/nezha.go:217
|
||||
msgid "IP Changed"
|
||||
msgstr "IP Changed"
|
||||
|
||||
#: service/singleton/alertsentinel.go:159
|
||||
msgid "Incident"
|
||||
msgstr "Incident"
|
||||
|
||||
#: service/singleton/alertsentinel.go:169
|
||||
msgid "Resolved"
|
||||
msgstr "Resolved"
|
||||
|
||||
#: service/singleton/crontask.go:53
|
||||
msgid "Tasks failed to register: ["
|
||||
msgstr "Tasks failed to register: ["
|
||||
|
||||
#: service/singleton/crontask.go:60
|
||||
msgid ""
|
||||
"] These tasks will not execute properly. Fix them in the admin dashboard."
|
||||
msgstr ""
|
||||
"] These tasks will not execute properly. Fix them in the admin dashboard."
|
||||
|
||||
#: service/singleton/crontask.go:146 service/singleton/crontask.go:171
|
||||
#, c-format
|
||||
msgid "[Task failed] %s: server %s is offline and cannot execute the task"
|
||||
msgstr "[Task failed] %s: server %s is offline and cannot execute the task"
|
||||
|
||||
#: service/singleton/servicesentinel.go:439
|
||||
#, c-format
|
||||
msgid "[Latency] %s %2f > %2f, Reporter: %s"
|
||||
msgstr "[Latency] %s %2f > %2f, Reporter: %s"
|
||||
|
||||
#: service/singleton/servicesentinel.go:446
|
||||
#, c-format
|
||||
msgid "[Latency] %s %2f < %2f, Reporter: %s"
|
||||
msgstr "[Latency] %s %2f < %2f, Reporter: %s"
|
||||
|
||||
#: service/singleton/servicesentinel.go:472
|
||||
#, c-format
|
||||
msgid "[%s] %s Reporter: %s, Error: %s"
|
||||
msgstr "[%s] %s Reporter: %s, Error: %s"
|
||||
|
||||
#: service/singleton/servicesentinel.go:515
|
||||
#, c-format
|
||||
msgid "[TLS] Fetch cert info failed, Reporter: %s, Error: %s"
|
||||
msgstr "[TLS] Fetch cert info failed, Reporter: %s, Error: %s"
|
||||
|
||||
#: service/singleton/servicesentinel.go:555
|
||||
#, c-format
|
||||
msgid "The TLS certificate will expire within seven days. Expiration time: %s"
|
||||
msgstr "The TLS certificate will expire within seven days. Expiration time: %s"
|
||||
|
||||
#: service/singleton/servicesentinel.go:568
|
||||
#, c-format
|
||||
msgid ""
|
||||
"TLS certificate changed, old: issuer %s, expires at %s; new: issuer %s, "
|
||||
"expires at %s"
|
||||
msgstr ""
|
||||
"TLS certificate changed, old: issuer %s, expires at %s; new: issuer %s, "
|
||||
"expires at %s"
|
||||
|
||||
#: service/singleton/servicesentinel.go:604
|
||||
msgid "No Data"
|
||||
msgstr "No Data"
|
||||
|
||||
#: service/singleton/servicesentinel.go:606
|
||||
msgid "Good"
|
||||
msgstr "Good"
|
||||
|
||||
#: service/singleton/servicesentinel.go:608
|
||||
msgid "Low Availability"
|
||||
msgstr "Low Availability"
|
||||
|
||||
#: service/singleton/servicesentinel.go:610
|
||||
msgid "Down"
|
||||
msgstr "Down"
|
BIN
pkg/i18n/translations/zh_CN/LC_MESSAGES/nezha.mo
Normal file
BIN
pkg/i18n/translations/zh_CN/LC_MESSAGES/nezha.mo
Normal file
Binary file not shown.
222
pkg/i18n/translations/zh_CN/LC_MESSAGES/nezha.po
Normal file
222
pkg/i18n/translations/zh_CN/LC_MESSAGES/nezha.po
Normal file
@ -0,0 +1,222 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-11-23 23:56+0800\n"
|
||||
"PO-Revision-Date: 2024-11-01 13:20+0800\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Language: zh_CN\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 3.5\n"
|
||||
|
||||
#: cmd/dashboard/controller/alertrule.go:100
|
||||
#, c-format
|
||||
msgid "alert id %d does not exist"
|
||||
msgstr "告警 ID %d 不存在"
|
||||
|
||||
#: cmd/dashboard/controller/alertrule.go:155
|
||||
msgid "duration need to be at least 3"
|
||||
msgstr "duration 至少为 3"
|
||||
|
||||
#: cmd/dashboard/controller/alertrule.go:159
|
||||
msgid "cycle_interval need to be at least 1"
|
||||
msgstr "cycle_interval 至少为 1"
|
||||
|
||||
#: cmd/dashboard/controller/alertrule.go:162
|
||||
msgid "cycle_start is not set"
|
||||
msgstr "cycle_start 未设置"
|
||||
|
||||
#: cmd/dashboard/controller/alertrule.go:165
|
||||
msgid "cycle_start is a future value"
|
||||
msgstr "cycle_start 是未来值"
|
||||
|
||||
#: cmd/dashboard/controller/alertrule.go:170
|
||||
msgid "need to configure at least a single rule"
|
||||
msgstr "需要至少定义一条规则"
|
||||
|
||||
#: cmd/dashboard/controller/controller.go:195
|
||||
msgid "database error"
|
||||
msgstr "数据库错误"
|
||||
|
||||
#: cmd/dashboard/controller/cron.go:63 cmd/dashboard/controller/cron.go:122
|
||||
msgid "scheduled tasks cannot be triggered by alarms"
|
||||
msgstr "计划任务不能被告警触发"
|
||||
|
||||
#: cmd/dashboard/controller/cron.go:161
|
||||
#, c-format
|
||||
msgid "task id %d does not exist"
|
||||
msgstr "任务 id %d 不存在"
|
||||
|
||||
#: cmd/dashboard/controller/ddns.go:56 cmd/dashboard/controller/ddns.go:120
|
||||
msgid "the retry count must be an integer between 1 and 10"
|
||||
msgstr "重试次数必须为大于 1 且不超过 10 的整数"
|
||||
|
||||
#: cmd/dashboard/controller/ddns.go:79 cmd/dashboard/controller/ddns.go:148
|
||||
msgid "error parsing %s: %v"
|
||||
msgstr "解析 %s 时发生错误:%v"
|
||||
|
||||
#: cmd/dashboard/controller/ddns.go:125 cmd/dashboard/controller/nat.go:95
|
||||
#, c-format
|
||||
msgid "profile id %d does not exist"
|
||||
msgstr "配置 id %d 不存在"
|
||||
|
||||
#: cmd/dashboard/controller/fm.go:45 cmd/dashboard/controller/terminal.go:43
|
||||
msgid "server not found or not connected"
|
||||
msgstr "服务器未找到或仍未连接"
|
||||
|
||||
#: cmd/dashboard/controller/notification.go:67
|
||||
#: cmd/dashboard/controller/notification.go:125
|
||||
msgid "a test message"
|
||||
msgstr "一条测试信息"
|
||||
|
||||
#: cmd/dashboard/controller/notification.go:106
|
||||
#, c-format
|
||||
msgid "notification id %d does not exist"
|
||||
msgstr "通知方式 id %d 不存在"
|
||||
|
||||
#: cmd/dashboard/controller/notification_group.go:80
|
||||
#: cmd/dashboard/controller/notification_group.go:142
|
||||
msgid "have invalid notification id"
|
||||
msgstr "通知方式 id 无效"
|
||||
|
||||
#: cmd/dashboard/controller/notification_group.go:131
|
||||
#: cmd/dashboard/controller/server_group.go:130
|
||||
#, c-format
|
||||
msgid "group id %d does not exist"
|
||||
msgstr "组 id %d 不存在"
|
||||
|
||||
#: cmd/dashboard/controller/server.go:60
|
||||
#, c-format
|
||||
msgid "server id %d does not exist"
|
||||
msgstr "服务器 id %d 不存在"
|
||||
|
||||
#: cmd/dashboard/controller/server_group.go:78
|
||||
#: cmd/dashboard/controller/server_group.go:139
|
||||
msgid "have invalid server id"
|
||||
msgstr "服务器 id 无效"
|
||||
|
||||
#: cmd/dashboard/controller/service.go:79
|
||||
#: cmd/dashboard/controller/service.go:155
|
||||
msgid "server not found"
|
||||
msgstr "未找到服务器"
|
||||
|
||||
#: cmd/dashboard/controller/service.go:86 cmd/dashboard/controller/user.go:23
|
||||
msgid "unauthorized"
|
||||
msgstr "未授权"
|
||||
|
||||
#: cmd/dashboard/controller/service.go:247
|
||||
#, c-format
|
||||
msgid "service id %d does not exist"
|
||||
msgstr "服务 id %d 不存在"
|
||||
|
||||
#: cmd/dashboard/controller/user.go:66
|
||||
msgid "password length must be greater than 6"
|
||||
msgstr "密码长度必须大于 6"
|
||||
|
||||
#: cmd/dashboard/controller/user.go:69
|
||||
msgid "username can't be empty"
|
||||
msgstr "用户名不能为空"
|
||||
|
||||
#: service/rpc/io_stream.go:122
|
||||
msgid "timeout: no connection established"
|
||||
msgstr "超时:无连接建立"
|
||||
|
||||
#: service/rpc/io_stream.go:125
|
||||
msgid "timeout: user connection not established"
|
||||
msgstr "超时:用户连接未建立"
|
||||
|
||||
#: service/rpc/io_stream.go:128
|
||||
msgid "timeout: agent connection not established"
|
||||
msgstr "超时:agent 连接未建立"
|
||||
|
||||
#: service/rpc/nezha.go:58
|
||||
msgid "Scheduled Task Executed Successfully"
|
||||
msgstr "计划任务执行成功"
|
||||
|
||||
#: service/rpc/nezha.go:62
|
||||
msgid "Scheduled Task Executed Failed"
|
||||
msgstr "计划任务执行失败"
|
||||
|
||||
#: service/rpc/nezha.go:217
|
||||
msgid "IP Changed"
|
||||
msgstr "IP 变更"
|
||||
|
||||
#: service/singleton/alertsentinel.go:159
|
||||
msgid "Incident"
|
||||
msgstr "事件"
|
||||
|
||||
#: service/singleton/alertsentinel.go:169
|
||||
msgid "Resolved"
|
||||
msgstr "恢复"
|
||||
|
||||
#: service/singleton/crontask.go:53
|
||||
msgid "Tasks failed to register: ["
|
||||
msgstr "注册失败的任务:["
|
||||
|
||||
#: service/singleton/crontask.go:60
|
||||
msgid ""
|
||||
"] These tasks will not execute properly. Fix them in the admin dashboard."
|
||||
msgstr "这些任务将无法正常执行,请进入后台重新修改保存。"
|
||||
|
||||
#: service/singleton/crontask.go:146 service/singleton/crontask.go:171
|
||||
#, c-format
|
||||
msgid "[Task failed] %s: server %s is offline and cannot execute the task"
|
||||
msgstr "[任务失败] %s,服务器 %s 离线,无法执行"
|
||||
|
||||
#: service/singleton/servicesentinel.go:439
|
||||
#, c-format
|
||||
msgid "[Latency] %s %2f > %2f, Reporter: %s"
|
||||
msgstr "[延迟告警] %s %2f > %2f,报告服务: %s"
|
||||
|
||||
#: service/singleton/servicesentinel.go:446
|
||||
#, c-format
|
||||
msgid "[Latency] %s %2f < %2f, Reporter: %s"
|
||||
msgstr "[延迟告警] %s %2f < %2f,报告服务: %s"
|
||||
|
||||
#: service/singleton/servicesentinel.go:472
|
||||
#, c-format
|
||||
msgid "[%s] %s Reporter: %s, Error: %s"
|
||||
msgstr "[%s] %s 报告服务:%s,错误信息:%s"
|
||||
|
||||
#: service/singleton/servicesentinel.go:515
|
||||
#, c-format
|
||||
msgid "[TLS] Fetch cert info failed, Reporter: %s, Error: %s"
|
||||
msgstr "[TLS] 获取证书信息失败,报告服务:%s,错误信息:%s"
|
||||
|
||||
#: service/singleton/servicesentinel.go:555
|
||||
#, c-format
|
||||
msgid "The TLS certificate will expire within seven days. Expiration time: %s"
|
||||
msgstr "TLS 证书将在 7 天内过期。过期时间为:%s"
|
||||
|
||||
#: service/singleton/servicesentinel.go:568
|
||||
#, c-format
|
||||
msgid ""
|
||||
"TLS certificate changed, old: issuer %s, expires at %s; new: issuer %s, "
|
||||
"expires at %s"
|
||||
msgstr ""
|
||||
"TLS 证书发生更改,旧值:颁发者 %s,过期日 %s;新值:颁发者 %s,过期日 %s"
|
||||
|
||||
#: service/singleton/servicesentinel.go:604
|
||||
msgid "No Data"
|
||||
msgstr "无数据"
|
||||
|
||||
#: service/singleton/servicesentinel.go:606
|
||||
msgid "Good"
|
||||
msgstr "正常"
|
||||
|
||||
#: service/singleton/servicesentinel.go:608
|
||||
msgid "Low Availability"
|
||||
msgstr "低可用"
|
||||
|
||||
#: service/singleton/servicesentinel.go:610
|
||||
msgid "Down"
|
||||
msgstr "故障"
|
BIN
pkg/i18n/translations/zh_TW/LC_MESSAGES/nezha.mo
Normal file
BIN
pkg/i18n/translations/zh_TW/LC_MESSAGES/nezha.mo
Normal file
Binary file not shown.
222
pkg/i18n/translations/zh_TW/LC_MESSAGES/nezha.po
Normal file
222
pkg/i18n/translations/zh_TW/LC_MESSAGES/nezha.po
Normal file
@ -0,0 +1,222 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-11-23 23:56+0800\n"
|
||||
"PO-Revision-Date: 2024-11-01 13:19+0800\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Language: zh_TW\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 3.5\n"
|
||||
|
||||
#: cmd/dashboard/controller/alertrule.go:100
|
||||
#, c-format
|
||||
msgid "alert id %d does not exist"
|
||||
msgstr "告警 ID %d 不存在"
|
||||
|
||||
#: cmd/dashboard/controller/alertrule.go:155
|
||||
msgid "duration need to be at least 3"
|
||||
msgstr "duration 至少為 3"
|
||||
|
||||
#: cmd/dashboard/controller/alertrule.go:159
|
||||
msgid "cycle_interval need to be at least 1"
|
||||
msgstr "cycle_interval 至少為 1"
|
||||
|
||||
#: cmd/dashboard/controller/alertrule.go:162
|
||||
msgid "cycle_start is not set"
|
||||
msgstr "cycle_start 未設定"
|
||||
|
||||
#: cmd/dashboard/controller/alertrule.go:165
|
||||
msgid "cycle_start is a future value"
|
||||
msgstr "cycle_start 是未來值"
|
||||
|
||||
#: cmd/dashboard/controller/alertrule.go:170
|
||||
msgid "need to configure at least a single rule"
|
||||
msgstr "需要至少定義一條規則"
|
||||
|
||||
#: cmd/dashboard/controller/controller.go:195
|
||||
msgid "database error"
|
||||
msgstr "資料庫錯誤"
|
||||
|
||||
#: cmd/dashboard/controller/cron.go:63 cmd/dashboard/controller/cron.go:122
|
||||
msgid "scheduled tasks cannot be triggered by alarms"
|
||||
msgstr "排程任務不能被告警觸發"
|
||||
|
||||
#: cmd/dashboard/controller/cron.go:161
|
||||
#, c-format
|
||||
msgid "task id %d does not exist"
|
||||
msgstr "任務 id %d 不存在"
|
||||
|
||||
#: cmd/dashboard/controller/ddns.go:56 cmd/dashboard/controller/ddns.go:120
|
||||
msgid "the retry count must be an integer between 1 and 10"
|
||||
msgstr "重試次數必須為大於 1 且不超過 10 的整數"
|
||||
|
||||
#: cmd/dashboard/controller/ddns.go:79 cmd/dashboard/controller/ddns.go:148
|
||||
msgid "error parsing %s: %v"
|
||||
msgstr "解析 %s 時發生錯誤:%v"
|
||||
|
||||
#: cmd/dashboard/controller/ddns.go:125 cmd/dashboard/controller/nat.go:95
|
||||
#, c-format
|
||||
msgid "profile id %d does not exist"
|
||||
msgstr "配置 id %d 不存在"
|
||||
|
||||
#: cmd/dashboard/controller/fm.go:45 cmd/dashboard/controller/terminal.go:43
|
||||
msgid "server not found or not connected"
|
||||
msgstr "伺服器未找到或仍未連線"
|
||||
|
||||
#: cmd/dashboard/controller/notification.go:67
|
||||
#: cmd/dashboard/controller/notification.go:125
|
||||
msgid "a test message"
|
||||
msgstr "一條測試資訊"
|
||||
|
||||
#: cmd/dashboard/controller/notification.go:106
|
||||
#, c-format
|
||||
msgid "notification id %d does not exist"
|
||||
msgstr "通知方式 id %d 不存在"
|
||||
|
||||
#: cmd/dashboard/controller/notification_group.go:80
|
||||
#: cmd/dashboard/controller/notification_group.go:142
|
||||
msgid "have invalid notification id"
|
||||
msgstr "通知方式 id 無效"
|
||||
|
||||
#: cmd/dashboard/controller/notification_group.go:131
|
||||
#: cmd/dashboard/controller/server_group.go:130
|
||||
#, c-format
|
||||
msgid "group id %d does not exist"
|
||||
msgstr "組 id %d 不存在"
|
||||
|
||||
#: cmd/dashboard/controller/server.go:60
|
||||
#, c-format
|
||||
msgid "server id %d does not exist"
|
||||
msgstr "伺服器 id %d 不存在"
|
||||
|
||||
#: cmd/dashboard/controller/server_group.go:78
|
||||
#: cmd/dashboard/controller/server_group.go:139
|
||||
msgid "have invalid server id"
|
||||
msgstr "伺服器 id 無效"
|
||||
|
||||
#: cmd/dashboard/controller/service.go:79
|
||||
#: cmd/dashboard/controller/service.go:155
|
||||
msgid "server not found"
|
||||
msgstr "未找到伺服器"
|
||||
|
||||
#: cmd/dashboard/controller/service.go:86 cmd/dashboard/controller/user.go:23
|
||||
msgid "unauthorized"
|
||||
msgstr "未授權"
|
||||
|
||||
#: cmd/dashboard/controller/service.go:247
|
||||
#, c-format
|
||||
msgid "service id %d does not exist"
|
||||
msgstr "服務 id %d 不存在"
|
||||
|
||||
#: cmd/dashboard/controller/user.go:66
|
||||
msgid "password length must be greater than 6"
|
||||
msgstr "密碼長度必須大於 6"
|
||||
|
||||
#: cmd/dashboard/controller/user.go:69
|
||||
msgid "username can't be empty"
|
||||
msgstr "使用者名稱不能為空"
|
||||
|
||||
#: service/rpc/io_stream.go:122
|
||||
msgid "timeout: no connection established"
|
||||
msgstr "超時:無連線建立"
|
||||
|
||||
#: service/rpc/io_stream.go:125
|
||||
msgid "timeout: user connection not established"
|
||||
msgstr "超時:使用者連線未建立"
|
||||
|
||||
#: service/rpc/io_stream.go:128
|
||||
msgid "timeout: agent connection not established"
|
||||
msgstr "超時:agent 連線未建立"
|
||||
|
||||
#: service/rpc/nezha.go:58
|
||||
msgid "Scheduled Task Executed Successfully"
|
||||
msgstr "排程任務執行成功"
|
||||
|
||||
#: service/rpc/nezha.go:62
|
||||
msgid "Scheduled Task Executed Failed"
|
||||
msgstr "排程任務執行失敗"
|
||||
|
||||
#: service/rpc/nezha.go:217
|
||||
msgid "IP Changed"
|
||||
msgstr "IP 變更"
|
||||
|
||||
#: service/singleton/alertsentinel.go:159
|
||||
msgid "Incident"
|
||||
msgstr "事件"
|
||||
|
||||
#: service/singleton/alertsentinel.go:169
|
||||
msgid "Resolved"
|
||||
msgstr "恢復"
|
||||
|
||||
#: service/singleton/crontask.go:53
|
||||
msgid "Tasks failed to register: ["
|
||||
msgstr "註冊失敗的任務:["
|
||||
|
||||
#: service/singleton/crontask.go:60
|
||||
msgid ""
|
||||
"] These tasks will not execute properly. Fix them in the admin dashboard."
|
||||
msgstr "這些任務將無法正常執行,請進入後台重新修改儲存。"
|
||||
|
||||
#: service/singleton/crontask.go:146 service/singleton/crontask.go:171
|
||||
#, c-format
|
||||
msgid "[Task failed] %s: server %s is offline and cannot execute the task"
|
||||
msgstr "[任務失敗] %s,伺服器 %s 離線,無法執行"
|
||||
|
||||
#: service/singleton/servicesentinel.go:439
|
||||
#, c-format
|
||||
msgid "[Latency] %s %2f > %2f, Reporter: %s"
|
||||
msgstr "[延遲告警] %s %2f > %2f,報告服務: %s"
|
||||
|
||||
#: service/singleton/servicesentinel.go:446
|
||||
#, c-format
|
||||
msgid "[Latency] %s %2f < %2f, Reporter: %s"
|
||||
msgstr "[延遲告警] %s %2f < %2f,報告服務: %s"
|
||||
|
||||
#: service/singleton/servicesentinel.go:472
|
||||
#, c-format
|
||||
msgid "[%s] %s Reporter: %s, Error: %s"
|
||||
msgstr "[%s] %s 報告服務:%s,錯誤資訊:%s"
|
||||
|
||||
#: service/singleton/servicesentinel.go:515
|
||||
#, c-format
|
||||
msgid "[TLS] Fetch cert info failed, Reporter: %s, Error: %s"
|
||||
msgstr "[TLS] 獲取證書資訊失敗,報告服務:%s,錯誤資訊:%s"
|
||||
|
||||
#: service/singleton/servicesentinel.go:555
|
||||
#, c-format
|
||||
msgid "The TLS certificate will expire within seven days. Expiration time: %s"
|
||||
msgstr "TLS 證書將在 7 天內過期。過期時間為:%s"
|
||||
|
||||
#: service/singleton/servicesentinel.go:568
|
||||
#, c-format
|
||||
msgid ""
|
||||
"TLS certificate changed, old: issuer %s, expires at %s; new: issuer %s, "
|
||||
"expires at %s"
|
||||
msgstr ""
|
||||
"TLS 證書發生更改,舊值:頒發者 %s,過期日 %s;新值:頒發者 %s,過期日 %s"
|
||||
|
||||
#: service/singleton/servicesentinel.go:604
|
||||
msgid "No Data"
|
||||
msgstr "無資料"
|
||||
|
||||
#: service/singleton/servicesentinel.go:606
|
||||
msgid "Good"
|
||||
msgstr "正常"
|
||||
|
||||
#: service/singleton/servicesentinel.go:608
|
||||
msgid "Low Availability"
|
||||
msgstr "低可用"
|
||||
|
||||
#: service/singleton/servicesentinel.go:610
|
||||
msgid "Down"
|
||||
msgstr "故障"
|
@ -1,83 +0,0 @@
|
||||
package mygin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/naiba/nezha/model"
|
||||
"github.com/naiba/nezha/service/singleton"
|
||||
)
|
||||
|
||||
type AuthorizeOption struct {
|
||||
GuestOnly bool
|
||||
MemberOnly bool
|
||||
IsPage bool
|
||||
AllowAPI bool
|
||||
Msg string
|
||||
Redirect string
|
||||
Btn string
|
||||
}
|
||||
|
||||
func Authorize(opt AuthorizeOption) func(*gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
var code = http.StatusForbidden
|
||||
if opt.GuestOnly {
|
||||
code = http.StatusBadRequest
|
||||
}
|
||||
|
||||
commonErr := ErrInfo{
|
||||
Title: "访问受限",
|
||||
Code: code,
|
||||
Msg: opt.Msg,
|
||||
Link: opt.Redirect,
|
||||
Btn: opt.Btn,
|
||||
}
|
||||
var isLogin bool
|
||||
|
||||
// 用户鉴权
|
||||
token, _ := c.Cookie(singleton.Conf.Site.CookieName)
|
||||
token = strings.TrimSpace(token)
|
||||
if token != "" {
|
||||
var u model.User
|
||||
if err := singleton.DB.Where("token = ?", token).First(&u).Error; err == nil {
|
||||
isLogin = u.TokenExpired.After(time.Now())
|
||||
}
|
||||
if isLogin {
|
||||
c.Set(model.CtxKeyAuthorizedUser, &u)
|
||||
}
|
||||
}
|
||||
|
||||
// API鉴权
|
||||
if opt.AllowAPI {
|
||||
apiToken := c.GetHeader("Authorization")
|
||||
if apiToken != "" {
|
||||
var u model.User
|
||||
singleton.ApiLock.RLock()
|
||||
if _, ok := singleton.ApiTokenList[apiToken]; ok {
|
||||
err := singleton.DB.First(&u).Where("id = ?", singleton.ApiTokenList[apiToken].UserID).Error
|
||||
isLogin = err == nil
|
||||
}
|
||||
singleton.ApiLock.RUnlock()
|
||||
if isLogin {
|
||||
c.Set(model.CtxKeyAuthorizedUser, &u)
|
||||
c.Set("isAPI", true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 已登录且只能游客访问
|
||||
if isLogin && opt.GuestOnly {
|
||||
ShowErrorPage(c, commonErr, opt.IsPage)
|
||||
return
|
||||
}
|
||||
|
||||
// 未登录且需要登录
|
||||
if !isLogin && opt.MemberOnly {
|
||||
ShowErrorPage(c, commonErr, opt.IsPage)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
package mygin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/naiba/nezha/model"
|
||||
"github.com/naiba/nezha/service/singleton"
|
||||
)
|
||||
|
||||
type ErrInfo struct {
|
||||
Code int
|
||||
Title string
|
||||
Msg string
|
||||
Link string
|
||||
Btn string
|
||||
}
|
||||
|
||||
func ShowErrorPage(c *gin.Context, i ErrInfo, isPage bool) {
|
||||
if isPage {
|
||||
c.HTML(i.Code, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/error", CommonEnvironment(c, gin.H{
|
||||
"Code": i.Code,
|
||||
"Title": i.Title,
|
||||
"Msg": i.Msg,
|
||||
"Link": i.Link,
|
||||
"Btn": i.Btn,
|
||||
}))
|
||||
} else {
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: i.Code,
|
||||
Message: i.Msg,
|
||||
})
|
||||
}
|
||||
c.Abort()
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
package mygin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
|
||||
"github.com/naiba/nezha/model"
|
||||
"github.com/naiba/nezha/service/singleton"
|
||||
)
|
||||
|
||||
var adminPage = map[string]bool{
|
||||
"/server": true,
|
||||
"/monitor": true,
|
||||
"/setting": true,
|
||||
"/notification": true,
|
||||
"/ddns": true,
|
||||
"/nat": true,
|
||||
"/cron": true,
|
||||
"/api": true,
|
||||
}
|
||||
|
||||
func CommonEnvironment(c *gin.Context, data map[string]interface{}) gin.H {
|
||||
data["MatchedPath"] = c.MustGet("MatchedPath")
|
||||
data["Version"] = singleton.Version
|
||||
data["Conf"] = singleton.Conf
|
||||
data["Themes"] = model.Themes
|
||||
data["CustomCode"] = singleton.Conf.Site.CustomCode
|
||||
data["CustomCodeDashboard"] = singleton.Conf.Site.CustomCodeDashboard
|
||||
// 是否是管理页面
|
||||
data["IsAdminPage"] = adminPage[data["MatchedPath"].(string)]
|
||||
// 站点标题
|
||||
if t, has := data["Title"]; !has {
|
||||
data["Title"] = singleton.Conf.Site.Brand
|
||||
} else {
|
||||
data["Title"] = fmt.Sprintf("%s - %s", t, singleton.Conf.Site.Brand)
|
||||
}
|
||||
u, ok := c.Get(model.CtxKeyAuthorizedUser)
|
||||
if ok {
|
||||
data["Admin"] = u
|
||||
}
|
||||
data["LANG"] = map[string]string{
|
||||
"Add": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Add"}),
|
||||
"Edit": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Edit"}),
|
||||
"AlarmRule": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "AlarmRule"}),
|
||||
"Notification": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "NotificationMethod"}),
|
||||
"Server": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Server"}),
|
||||
"Monitor": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ServicesManagement"}),
|
||||
"Cron": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ScheduledTasks"}),
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func RecordPath(c *gin.Context) {
|
||||
url := c.Request.URL.String()
|
||||
for _, p := range c.Params {
|
||||
url = strings.Replace(url, p.Value, ":"+p.Key, 1)
|
||||
}
|
||||
c.Set("MatchedPath", url)
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
package mygin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/naiba/nezha/model"
|
||||
"github.com/naiba/nezha/pkg/utils"
|
||||
"github.com/naiba/nezha/service/singleton"
|
||||
)
|
||||
|
||||
func PreferredTheme(c *gin.Context) {
|
||||
// 采用前端传入的主题
|
||||
if theme, err := c.Cookie("preferred_theme"); err == nil {
|
||||
if _, has := model.Themes[theme]; has {
|
||||
// 检验自定义主题
|
||||
if theme == "custom" && singleton.Conf.Site.Theme != "custom" && !utils.IsFileExists("resource/template/theme-custom/home.html") {
|
||||
return
|
||||
}
|
||||
c.Set(model.CtxKeyPreferredTheme, theme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GetPreferredTheme(c *gin.Context, path string) string {
|
||||
if theme, has := c.Get(model.CtxKeyPreferredTheme); has {
|
||||
return fmt.Sprintf("theme-%s%s", theme, path)
|
||||
}
|
||||
return fmt.Sprintf("theme-%s%s", singleton.Conf.Site.Theme, path)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user