Compare commits
379 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e3db107ec | |||
| 81843470b7 | |||
| a539532ae2 | |||
| fc1584e7da | |||
| 3c72a66c1f | |||
| 346ec7ab1f | |||
| 7850b74966 | |||
| f2d2bad52f | |||
| 0786451c79 | |||
| 0291ae7637 | |||
| de76376e97 | |||
| de21179d61 | |||
| b81f3de01d | |||
| be3bae7d57 | |||
| ffb4e21f48 | |||
| 39624d8e1c | |||
| 1378413e54 | |||
| 730aea037c | |||
| 5ae6135429 | |||
| 84703391a3 | |||
| 354705ec5b | |||
| f182eab2dc | |||
| 2828795fb0 | |||
| e6aad21631 | |||
| ff239c467a | |||
| 892f91be3e | |||
| d51d11f460 | |||
| f9233bd36b | |||
| a2ebc070ae | |||
| 480a6a829d | |||
| 47b92a8b2a | |||
| 1a7f679fca | |||
| 8b6b94ea1f | |||
| 0d9c8b3a2d | |||
| 2be97a4543 | |||
| 7b6b86f0f6 | |||
| 6f82242d92 | |||
| 7f301cec58 | |||
| c75cfe9eb6 | |||
| b94664ed87 | |||
| 93fc5e08f6 | |||
| 9bcfd4cd73 | |||
| 2d10110eef | |||
| 78e31fdc40 | |||
| d2b1394950 | |||
| 389b0a005b | |||
| 0b1554c2c6 | |||
| 3b580e2827 | |||
| 595f5ba2d9 | |||
| 78543dac67 | |||
| e9cbc8e945 | |||
| 905e206f9c | |||
| b5b1992e3b | |||
| a7fe0c87a5 | |||
| 5dcbac1c93 | |||
| 67a6d5cea8 | |||
| f6fec06e82 | |||
| 2d74e8966e | |||
| 065877547e | |||
| 12b6a1af35 | |||
| b36f7f3085 | |||
| 2112e86329 | |||
| f2c3569447 | |||
| 1a9fe40b80 | |||
| df021819b4 | |||
| 57749f03b9 | |||
| d97697efde | |||
| bc28719b10 | |||
| db79d17919 | |||
| 4e6654af33 | |||
| 4015c7e1bc | |||
| d30874a5d4 | |||
| f4a97c1740 | |||
| 7cb497929b | |||
| 8cb74159f2 | |||
| 3b75f7d2ac | |||
| 87c65176aa | |||
| 3b3cb1ff24 | |||
| a701ebcad6 | |||
| 3d88983feb | |||
| 39a9bad2e1 | |||
| 7b0e98bf02 | |||
| 324c61625d | |||
| b50a3f4b4e | |||
| 3de3b40d88 | |||
| 722c4f3812 | |||
| 9cf3a0e2b6 | |||
| c80c22c133 | |||
| 5acf1e6eb7 | |||
| cdeaf9f48f | |||
| afebeb7c00 | |||
| 0d73f2919d | |||
| 070ad97e95 | |||
| b7eba92b2b | |||
| 2c339b9923 | |||
| 8d0ea50d4f | |||
| d6a1c13328 | |||
| 789e6e2043 | |||
| d692b9ac13 | |||
| eb362e42e3 | |||
| 3ca106ec87 | |||
| 377baa0783 | |||
| 117ccabbb5 | |||
| b718c11f3a | |||
| 41d3d3fb97 | |||
| 31ad944da8 | |||
| a25de6e4db | |||
| 3af6d4d47d | |||
| 59a2fef9e8 | |||
| 3d22a1bc03 | |||
| e64c27a821 | |||
| f2bdba10bb | |||
| e18d07e694 | |||
| 50ba0ce38d | |||
| 8b2c7ea778 | |||
| e640a3cedd | |||
| 31ab4f0fb6 | |||
| 8dee17b539 | |||
| af3172739e | |||
| 182d9b1c8c | |||
| d03facb556 | |||
| 577fb370a7 | |||
| 7ef8fc6787 | |||
| e5ba1375b1 | |||
| e8a01e2d04 | |||
| bdee27541c | |||
| f807da175d | |||
| ae29541d02 | |||
| 0f844ac3ea | |||
| 744d0b9617 | |||
| 1bea44146f | |||
| a7a68f6b34 | |||
| 80bfe50335 | |||
| f235492f8b | |||
| e1bcabec5d | |||
| 8a03006772 | |||
| e497f5f7d7 | |||
| 5668bc1afd | |||
| bf9c0bc12e | |||
| 7677c3b50e | |||
| 82a74a538b | |||
| d84e3f75e3 | |||
| ce155af56b | |||
| ebc5fb9796 | |||
| 5e237a6bce | |||
| 567663a6f5 | |||
| 38935607d6 | |||
| 0af6b22862 | |||
| 787bb80355 | |||
| 1785b8ee1a | |||
| a223280172 | |||
| e1cceca340 | |||
| b9d5aa5fb1 | |||
| 3fae54bf58 | |||
| 1b2f9ea1d2 | |||
| fb94c015b2 | |||
| 1ae8011660 | |||
| 2978df4981 | |||
| 99cd473680 | |||
| d18bf93571 | |||
| 86e4cff989 | |||
| 6072332e1a | |||
| 8721cff4df | |||
| cab338c4c4 | |||
| de7dd8834f | |||
| 1d9e2761f3 | |||
| 6102742377 | |||
| 641747230b | |||
| 3c1c4e92b9 | |||
| 6ec5ebb18d | |||
| 198bd39664 | |||
| b561e241a5 | |||
| ff3b755faa | |||
| 7e7b180370 | |||
| 497e357d65 | |||
| 09698f7cd3 | |||
| 119183971d | |||
| 96d7e14631 | |||
| 0c4d97a512 | |||
| 3c39027120 | |||
| 8f6b2647a3 | |||
| ecbf8dd51a | |||
| 8a8e6302da | |||
| 2e45b6c5ab | |||
| 962098b1f7 | |||
| 7817756569 | |||
| 6738f3e83d | |||
| 07bb0961da | |||
| ac6c57b394 | |||
| 5082a4f9f9 | |||
| cfc1d46381 | |||
| 3157011cc0 | |||
| 48b06b25da | |||
| f04ae48347 | |||
| 87d26fbb23 | |||
| 6cfb7f4e0b | |||
| bee61facad | |||
| 342bf62ef6 | |||
| 9cd80f8d1b | |||
| f9b53175a1 | |||
| 6704612482 | |||
| d31f40f2dc | |||
| e02de8ba05 | |||
| 95f0e0d536 | |||
| 7e18d75203 | |||
| 3de66d884c | |||
| a911002953 | |||
| c5d0b31251 | |||
| f77e4d17fc | |||
| 6892cff96f | |||
| 4337bc2718 | |||
| 82145021b7 | |||
| d0d98f273c | |||
| 7ff18ae0a9 | |||
| 7a5d2352fa | |||
| dc0314fc28 | |||
| 9ca2445d4a | |||
| ba42e33b72 | |||
| 06c0c08ead | |||
| dc96f9d4ac | |||
| e7a54cbe85 | |||
| 39f03106df | |||
| 24ec5d0bb3 | |||
| 22cd9f8ce1 | |||
| c5efc2fb33 | |||
| 24b633d24c | |||
| 6f994e2282 | |||
| 946a8198a3 | |||
| 39ff8c9040 | |||
| 65ebecafdb | |||
| af5e79ce37 | |||
| a37393b688 | |||
| c18a96e439 | |||
| 1208144605 | |||
| 07c1c110d9 | |||
| dd66274997 | |||
| dc0174c64e | |||
| 67055a5aac | |||
| 54470f87b0 | |||
| 41a61a2c15 | |||
| 17390a026f | |||
| 3fce818d13 | |||
| 169c0ff18e | |||
| 6a30d56d7d | |||
| 4ad7ec58be | |||
| d4feba17d0 | |||
| e5f36df4bf | |||
| e1b53e88ec | |||
| d12aabd4d3 | |||
| 54c893d6e6 | |||
| 4e01168f3c | |||
| 5bb43dbf0f | |||
| dcb0d667ea | |||
| af8ef1abc8 | |||
| 2fc19dd97e | |||
| b0e577b42a | |||
| 051b6b9227 | |||
| 92ac563d75 | |||
| b7b775971e | |||
| 32266ca7c8 | |||
| 73387d114e | |||
| 3271557561 | |||
| 9a295241c3 | |||
| a00b44c485 | |||
| 93b72ad45f | |||
| a86b35f587 | |||
| 4054c788f3 | |||
| 881eb7bfb1 | |||
| fcc35c19ee | |||
| 7c3b5c02ba | |||
| bb70b25565 | |||
| bc2e07bcd8 | |||
| aa74f30893 | |||
| 27687d7dbf | |||
| f6f21d7b70 | |||
| 0815507aa8 | |||
| fb40b140c9 | |||
| eedf07ec2a | |||
| 5c214625c3 | |||
| 0402805f24 | |||
| 40715dd8a3 | |||
| 505e6d4794 | |||
| 04f3925e6d | |||
| c77fa3f0ea | |||
| 51ede84e0a | |||
| 053ef46f95 | |||
| 11d58d838f | |||
| 13b15ccb32 | |||
| 255acb392c | |||
| 6c16901dbc | |||
| 284a4c67d4 | |||
| 484ad397e7 | |||
| 407203dcfa | |||
| dded5e26aa | |||
| d029e6c58e | |||
| c1c0cbbac5 | |||
| 0fffe3a644 | |||
| 2e26af435a | |||
| ef2ecfcc80 | |||
| 6fe4d82619 | |||
| 29562974ce | |||
| b7f169634b | |||
| 87e3011f18 | |||
| cc714a5007 | |||
| 615744e0cf | |||
| 3193a15131 | |||
| 194287554e | |||
| 97b1d1a4bb | |||
| b150f61862 | |||
| b82bc21d1b | |||
| 6e9a4963af | |||
| b4a3708fa0 | |||
| 9bca8173f2 | |||
| 6520aba081 | |||
| 2903d00550 | |||
| a53ba36db8 | |||
| e9f2854d59 | |||
| 43cd3bf38d | |||
| 325a218e20 | |||
| 847e55bc8b | |||
| 1ebddd289c | |||
| e3f8fe8fe9 | |||
| b9996ba41e | |||
| bb1cd025f1 | |||
| a3059eb1cb | |||
| 192a1044ed | |||
| bbc6e3c7cc | |||
| 0aaea53af6 | |||
| 2ad9f48a5b | |||
| 0e715a7576 | |||
| 724610a45d | |||
| f65cf9e38e | |||
| 4a8ebfbdfa | |||
| fee0fcb998 | |||
| 1e081fcdf2 | |||
| 6c6fb09270 | |||
| 51b47818c0 | |||
| 5b393819ba | |||
| f807d0cd1a | |||
| 0d6f0f317d | |||
| 6bad7f4b5b | |||
| 7a7134b26a | |||
| 28cf7098ff | |||
| b56979147e | |||
| 2ec0d9e844 | |||
| 66641a626e | |||
| 95bfd6843b | |||
| 7e28a34b7f | |||
| b2152edeeb | |||
| 00815979d2 | |||
| c4e01b37b7 | |||
| 7907345521 | |||
| 8f32d6cdb0 | |||
| c3582903bf | |||
| 7d25d2f5aa | |||
| 46fc7b1c6a | |||
| 34ac62d1ae | |||
| 30e78ccf46 | |||
| 22f4cc7fcb | |||
| 1771ae682d | |||
| d1beb60ffd | |||
| cba5f86667 | |||
| e1d9832ce0 | |||
| 6278535902 | |||
| 32fb87243b | |||
| 5f85e7fbe0 | |||
| c36104a075 | |||
| 583f5d13c7 | |||
| b453738daf | |||
| 98431fb4b2 | |||
| 4d76e9286d | |||
| c70758339f | |||
| ff524cdb0b | |||
| 73c95d880a | |||
| 8a1d8c0b05 | |||
| 2251175906 | |||
| 69edde98c8 | |||
| 29a3db640d | |||
| ee87bf65b4 |
@@ -0,0 +1,69 @@
|
||||
# Config file for [Air](https://github.com/air-verse/air) in TOML format
|
||||
|
||||
# Working directory
|
||||
# . or absolute path, please note that the directories following must be under root.
|
||||
root = "."
|
||||
tmp_dir = "storage/temp"
|
||||
|
||||
[build]
|
||||
# Array of commands to run before each build
|
||||
pre_cmd = []
|
||||
# Just plain old shell command. You could use `make` as well.
|
||||
cmd = "go build -o storage/temp/main.exe ./cmd/web"
|
||||
# Array of commands to run after ^C
|
||||
post_cmd = []
|
||||
# Binary file yields from `cmd`.
|
||||
bin = "storage/temp/main.exe"
|
||||
# Customize binary, can setup environment variables when run your app.
|
||||
full_bin = ""
|
||||
# Watch these filename extensions.
|
||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||
# Ignore these filename extensions or directories.
|
||||
exclude_dir = ["storage", "web"]
|
||||
# Watch these directories if you specified.
|
||||
include_dir = []
|
||||
# Watch these files.
|
||||
include_file = []
|
||||
# Exclude files.
|
||||
exclude_file = []
|
||||
# Exclude specific regular expressions.
|
||||
exclude_regex = ["_test\\.go"]
|
||||
# Exclude unchanged files.
|
||||
exclude_unchanged = true
|
||||
# Follow symlink for directories
|
||||
follow_symlink = true
|
||||
# This log file places in your tmp_dir.
|
||||
log = "build-errors.log"
|
||||
# It's not necessary to trigger build each time file changes if it's too frequent.
|
||||
delay = 2000
|
||||
# Stop running old binary when build errors occur.
|
||||
stop_on_error = true
|
||||
# Send Interrupt signal before killing process (windows does not support this feature)
|
||||
send_interrupt = false
|
||||
# Delay after sending Interrupt signal
|
||||
kill_delay = 500 # nanosecond
|
||||
# Rerun binary or not
|
||||
rerun = false
|
||||
# Delay after each execution
|
||||
rerun_delay = 500
|
||||
|
||||
[log]
|
||||
# Show log time
|
||||
time = false
|
||||
# Only show main log (silences watcher, build, runner)
|
||||
main_only = false
|
||||
|
||||
[color]
|
||||
# Customize each part's color. If no color found, use the raw app log.
|
||||
main = "magenta"
|
||||
watcher = "cyan"
|
||||
build = "yellow"
|
||||
runner = "green"
|
||||
|
||||
[misc]
|
||||
# Delete tmp directory on exit
|
||||
clean_on_exit = true
|
||||
|
||||
[screen]
|
||||
clear_on_rebuild = true
|
||||
keep_scroll = true
|
||||
@@ -0,0 +1,6 @@
|
||||
*.sh linguist-language=Go
|
||||
*.ts linguist-language=Go
|
||||
*.js linguist-language=Go
|
||||
*.css linguist-language=Go
|
||||
*.scss linguist-language=Go
|
||||
*.html linguist-language=Go
|
||||
@@ -1,4 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
open_collective: tnb
|
||||
custom: [ 'https://afdian.net/a/TheTNB' ]
|
||||
custom: [ 'https://afdian.com/a/TheTNB' ]
|
||||
|
||||
@@ -27,7 +27,7 @@ body:
|
||||
required: false
|
||||
- label: 这个问题可以被稳定复现 (The problem can be stably reproduced)
|
||||
required: false
|
||||
- label: 问题是在升级之后产生的 (The problem is generated after upgrading)
|
||||
- label: 问题是在更新之后产生的 (The problem is generated after upgrading)
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: system
|
||||
@@ -68,7 +68,7 @@ body:
|
||||
attributes:
|
||||
label: 描述问题 (Describe The Problem)
|
||||
description: |
|
||||
简明概要地描述你遇到的问题。
|
||||
简明概要地描述您遇到的问题。
|
||||
Briefly describe the problem you are having.
|
||||
validations:
|
||||
required: true
|
||||
@@ -91,7 +91,7 @@ body:
|
||||
attributes:
|
||||
label: 预期行为 (Expected Behavior)
|
||||
description: |
|
||||
简明概要地描述你期望发生的事情。
|
||||
简明概要地描述您期望发生的事情。
|
||||
Briefly describe what you expect to happen.
|
||||
validations:
|
||||
required: true
|
||||
@@ -104,8 +104,8 @@ body:
|
||||
Please copy and paste any relevant log output.
|
||||
可以把文件拖入这个区域以添加日志文件。
|
||||
Files can be dragged into this area to add log files.
|
||||
面板日志文件可在 `/www/panel/storage/logs` 中找到。
|
||||
Panel log files can be found in `/www/panel/storage/logs`.
|
||||
面板日志文件可在安装目录 `panel/storage/logs` 中找到。
|
||||
Panel log files can be found in the installation directory `panel/storage/logs`.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
@@ -113,7 +113,7 @@ body:
|
||||
attributes:
|
||||
label: 截图 (Screenshots)
|
||||
description: |
|
||||
如果有,添加屏幕截图可帮助更快定位你的问题。
|
||||
如果有,添加屏幕截图可帮助更快定位您的问题。
|
||||
If so, adding screenshots can help locate your issue faster.
|
||||
可以复制图片后在此区域内粘贴以添加图片。
|
||||
Pictures can be copied and pasted in this area to add pictures.
|
||||
@@ -128,7 +128,7 @@ body:
|
||||
description: |
|
||||
运行环境?浏览器?软件版本?相关的配置?链接?参考资料?
|
||||
Environment? Browser? Software version? Related configuration? Link? References?
|
||||
任何能让我们对你所遇到的问题有更多了解的东西。
|
||||
任何能让我们对您所遇到的问题有更多了解的东西。
|
||||
Anything that can give us more insight into the problem you're having.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
@@ -25,7 +25,7 @@ body:
|
||||
attributes:
|
||||
label: 描述功能 (Describe Feature)
|
||||
description: |
|
||||
简明概要地描述你的新功能,以及它将解决什么问题。
|
||||
简明概要地描述您的新功能,以及它将解决什么问题。
|
||||
Briefly describe your new feature and what problem it will solve.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 819 KiB |
|
After Width: | Height: | Size: 64 KiB |
@@ -0,0 +1,55 @@
|
||||
name: Backend
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
goarch: [ amd64, arm64 ]
|
||||
fail-fast: true
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
cache: true
|
||||
go-version: 'stable'
|
||||
- name: Install dependencies
|
||||
run: go mod tidy
|
||||
- name: Wait for frontend build
|
||||
uses: lewagon/wait-on-check-action@v1.3.4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
check-name: 'build (frontend)'
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Download frontend
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
with:
|
||||
workflow: frontend.yml
|
||||
name: frontend
|
||||
path: internal/embed/frontend
|
||||
check_artifacts: true
|
||||
- name: Build ${{ matrix.goarch }}
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: linux
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
run: |
|
||||
go build -ldflags '-s -w --extldflags "-static"' -o web-${{ matrix.goarch }} ./cmd/web
|
||||
go build -ldflags '-s -w --extldflags "-static"' -o cli-${{ matrix.goarch }} ./cmd/cli
|
||||
- name: Compress ${{ matrix.goarch }}
|
||||
run: |
|
||||
upx --best --lzma web-${{ matrix.goarch }}
|
||||
upx --best --lzma cli-${{ matrix.goarch }}
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.goarch }}
|
||||
path: |
|
||||
web-${{ matrix.goarch }}
|
||||
cli-${{ matrix.goarch }}
|
||||
@@ -1,32 +0,0 @@
|
||||
name: Build
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
goarch: [ amd64, arm64 ]
|
||||
fail-fast: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
cache: true
|
||||
go-version: '1.22'
|
||||
- name: Install dependencies
|
||||
run: go mod tidy
|
||||
- name: Build ${{ matrix.goarch }}
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: linux
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
run: go build -ldflags '-s -w --extldflags "-static"' -tags='nomsgpack' -o panel-${{ matrix.goarch }}
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: panel-${{ matrix.goarch }}
|
||||
path: panel-${{ matrix.goarch }}
|
||||
@@ -6,18 +6,21 @@ on:
|
||||
pull_request:
|
||||
jobs:
|
||||
codecov:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.22'
|
||||
cache: true
|
||||
go-version: 'stable'
|
||||
- name: Install dependencies
|
||||
run: go mod tidy
|
||||
- name: Run tests with coverage
|
||||
run: go test -v -coverprofile="coverage.out" ./...
|
||||
run: sudo go test -v -coverprofile="coverage.out" ./...
|
||||
- name: Upload coverage report to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
file: ./coverage.out
|
||||
token: ${{ secrets.CODECOV }}
|
||||
token: ${{ secrets.CODECOV }}
|
||||
@@ -0,0 +1,42 @@
|
||||
name: Frontend
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
jobs:
|
||||
build:
|
||||
name: build (frontend)
|
||||
runs-on: ubuntu-24.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: latest
|
||||
run_install: true
|
||||
package_json_file: web/package.json
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: web/pnpm-lock.yaml
|
||||
- name: Build frontend
|
||||
# We need to run the dev server first to generate the auto-imports files
|
||||
run: |
|
||||
cp .env.production .env
|
||||
cp settings/proxy-config.example.ts settings/proxy-config.ts
|
||||
pnpm dev &
|
||||
sleep 5
|
||||
kill %1
|
||||
pnpm build
|
||||
- name: Upload frontend
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: frontend
|
||||
path: web/dist/ # https://github.com/actions/upload-artifact/issues/541
|
||||
@@ -7,27 +7,34 @@ permissions:
|
||||
contents: write
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.22'
|
||||
- name: Fetch Frontend
|
||||
run: |
|
||||
sudo apt-get install -y curl jq unzip zip
|
||||
curl -sSL https://api.github.com/repos/TheTNB/panel-frontend/releases/latest | jq -r ".assets[] | select(.name | contains(\"dist\")) | .browser_download_url" | xargs curl -L -o frontend.zip
|
||||
rm -rf public
|
||||
unzip frontend.zip
|
||||
mv dist public
|
||||
cache: true
|
||||
go-version: 'stable'
|
||||
- name: Wait for frontend build
|
||||
uses: lewagon/wait-on-check-action@v1.3.4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
check-name: 'build (frontend)'
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Download frontend
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
with:
|
||||
workflow: frontend.yml
|
||||
name: frontend
|
||||
path: internal/embed/frontend
|
||||
check_artifacts: true
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
version: latest
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,25 @@
|
||||
name: Issue Auto Lock
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 */6 * * *"
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
issues:
|
||||
types: [ opened ]
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
jobs:
|
||||
issue-lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set GH_REPO
|
||||
run: echo "GH_REPO=${{ github.repository }}" >> $GITHUB_ENV
|
||||
- name: Auto Lock Issue
|
||||
uses: devhaozi/issue-auto-lock@v1
|
||||
with:
|
||||
gh_repo: ${{ github.repository }}
|
||||
gh_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue_labels: "⭐ No Star"
|
||||
@@ -1,18 +1,15 @@
|
||||
name: Issue Auto Reply
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [ labeled ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
issue-reply:
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: ✏️ Feature
|
||||
if: github.event.label.name == '✏️ Feature'
|
||||
@@ -27,7 +24,7 @@ jobs:
|
||||
我们认为您的建议非常有价值!欢迎提交 PR,请包含相应的测试用例、文档等,并确保 CI 通过,感谢和期待您的贡献!
|
||||
We think your suggestion is very valuable! Welcome to submit a PR, please include test cases, documentation, etc., and ensure that the CI is passed, thank you and look forward to your contribution!
|
||||
|
||||

|
||||

|
||||
- name: ☢️ Bug
|
||||
if: github.event.label.name == '☢️ Bug'
|
||||
uses: actions-cool/issues-helper@v3
|
||||
@@ -41,4 +38,4 @@ jobs:
|
||||
我们认为您的反馈非常有价值!欢迎提交 PR,请包含相应的测试用例、文档等,并确保 CI 通过,感谢和期待您的贡献!
|
||||
We think your feedback is very valuable! Welcome to submit a PR, please include test cases, documentation, etc., and ensure that the CI is passed, thank you and look forward to your contribution!
|
||||
|
||||

|
||||

|
||||
@@ -1,25 +0,0 @@
|
||||
name: Issue Close Question
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
issue-close-require:
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: needs more info
|
||||
uses: actions-cool/issues-helper@v3
|
||||
with:
|
||||
actions: 'close-issues'
|
||||
labels: 'question'
|
||||
inactive-day: 3
|
||||
body: |
|
||||
由于该 Issue 3天未收到回应,现已被自动关闭,若有任何问题,可评论回复。
|
||||
This issue has been closed automatically because it has not had recent activity for 3 days. If you have any questions, please comment here.
|
||||
@@ -7,18 +7,73 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
lint:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
golangci:
|
||||
name: golanci-lint
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.22'
|
||||
cache: false
|
||||
- name: Lint
|
||||
cache: true
|
||||
go-version: 'stable'
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
skip-cache: true
|
||||
version: latest
|
||||
args: --timeout=30m ./...
|
||||
nilaway:
|
||||
runs-on: ubuntu-24.04
|
||||
if: false
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
cache: true
|
||||
go-version: 'stable'
|
||||
- name: Install dependencies
|
||||
run: go mod tidy
|
||||
- name: Install NilAway
|
||||
run: go install go.uber.org/nilaway/cmd/nilaway@latest
|
||||
- name: Run NilAway
|
||||
run: nilaway -include-pkgs="github.com/TheTNB/panel" ./...
|
||||
govulncheck:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
cache: true
|
||||
go-version: 'stable'
|
||||
- name: Install Govulncheck
|
||||
run: go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
- name: Run Govulncheck
|
||||
run: govulncheck ./...
|
||||
frontend:
|
||||
runs-on: ubuntu-24.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: latest
|
||||
run_install: true
|
||||
package_json_file: web/package.json
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: web/pnpm-lock.yaml
|
||||
- name: Run pnpm lint
|
||||
run: pnpm lint
|
||||
@@ -5,19 +5,20 @@ on:
|
||||
- main
|
||||
pull_request:
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
unit:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.22'
|
||||
cache: true
|
||||
go-version: 'stable'
|
||||
- name: Install dependencies
|
||||
run: sudo apt-get install -y curl jq
|
||||
- name: Set up environment
|
||||
run: |
|
||||
cp panel-example.conf .env
|
||||
echo "DB_FILE=$(pwd)/storage/panel.db" >> .env
|
||||
go run . artisan key:generate
|
||||
cp config.example.yml config.yml
|
||||
- name: Run tests
|
||||
run: go test ./...
|
||||
run: sudo go test ./...
|
||||
@@ -1,10 +1,6 @@
|
||||
tmp
|
||||
/.air.toml
|
||||
/panel.conf
|
||||
|
||||
# Golang #
|
||||
# `go test -c` 生成的二进制文件
|
||||
*.test
|
||||
|
||||
# go coverage 工具
|
||||
*.out
|
||||
*.prof
|
||||
@@ -14,16 +10,15 @@ _cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
# 编译文件 #
|
||||
# 编译文件
|
||||
*.com
|
||||
*.class
|
||||
*.dll
|
||||
*.exe
|
||||
*.o
|
||||
*.so
|
||||
/panel
|
||||
|
||||
# 压缩包 #
|
||||
# 压缩包
|
||||
# Git 自带压缩,如果这些压缩包里有代码,建议解压后 commit
|
||||
*.7z
|
||||
*.dmg
|
||||
@@ -34,16 +29,17 @@ _cgo_export.*
|
||||
*.tar
|
||||
*.zip
|
||||
|
||||
# 日志文件和数据库 #
|
||||
# 日志文件和数据库及配置
|
||||
*.log
|
||||
*.sqlite
|
||||
*.db
|
||||
config.yml
|
||||
|
||||
# 临时文件 #
|
||||
# 临时文件
|
||||
tmp/
|
||||
.tmp/
|
||||
|
||||
# 系统生成文件 #
|
||||
# 系统生成文件
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
.AppleDouble
|
||||
@@ -58,7 +54,7 @@ Thumbs.db
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# IDE 和编辑器 #
|
||||
# IDE 和编辑器
|
||||
.idea/
|
||||
/go_build_*
|
||||
out/
|
||||
@@ -67,8 +63,3 @@ out/
|
||||
*.sublime*
|
||||
__debug_bin
|
||||
.project
|
||||
|
||||
# 前端工具链 #
|
||||
.sass-cache/*
|
||||
node_modules/
|
||||
/public
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
image: golang:1.22-bookworm
|
||||
|
||||
# 在每个任务执行前运行
|
||||
before_script:
|
||||
- mkdir -p .go
|
||||
- go version
|
||||
- go env -w GO111MODULE=on
|
||||
- go env -w GOPROXY=https://goproxy.cn,direct
|
||||
|
||||
.go_cache:
|
||||
variables:
|
||||
GOPATH: $CI_PROJECT_DIR/.go
|
||||
cache:
|
||||
paths:
|
||||
- .go/pkg/mod/
|
||||
|
||||
stages:
|
||||
- prepare
|
||||
- build
|
||||
- release
|
||||
|
||||
golangci_lint:
|
||||
stage: prepare
|
||||
image: golangci/golangci-lint:latest-alpine
|
||||
extends: .go_cache
|
||||
allow_failure: true
|
||||
script:
|
||||
- golangci-lint run --timeout 30m
|
||||
|
||||
unit_test:
|
||||
stage: prepare
|
||||
extends: .go_cache
|
||||
allow_failure: true
|
||||
script:
|
||||
- rm -rf /etc/apt/sources.list
|
||||
- rm -rf /etc/apt/sources.list.d/*
|
||||
- wget -O /etc/apt/sources.list https://mirrors.ustc.edu.cn/repogen/conf/debian-http-4-bookworm
|
||||
- apt-get update
|
||||
- apt-get install -y curl jq
|
||||
- cp panel-example.conf .env
|
||||
- echo "DB_FILE=$(pwd)/storage/panel.db" >> .env
|
||||
- go run . artisan key:generate
|
||||
- go test -v -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
|
||||
build:
|
||||
stage: build
|
||||
extends: .go_cache
|
||||
script:
|
||||
- go mod download
|
||||
- CGO_ENABLED=0 go build -ldflags '-s -w --extldflags "-static"' -tags='nomsgpack' -o panel
|
||||
artifacts:
|
||||
name: "panel"
|
||||
paths:
|
||||
- panel
|
||||
expire_in: 3 days
|
||||
|
||||
fetch:
|
||||
stage: build
|
||||
image: alpine:latest
|
||||
before_script:
|
||||
- sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
|
||||
- apk add --no-cache curl jq unzip zip
|
||||
script:
|
||||
- curl -sSL "https://git.haozi.net/api/v4/projects/opensource%2Fpanel-frontend/releases" | jq -r '.[0].assets.links[] | select(.name | contains("dist")) | .direct_asset_url' | xargs curl -L -o frontend.zip
|
||||
- rm -rf public
|
||||
- unzip frontend.zip
|
||||
- mv dist public
|
||||
artifacts:
|
||||
name: "frontend"
|
||||
paths:
|
||||
- public
|
||||
expire_in: 3 days
|
||||
|
||||
release:
|
||||
stage: release
|
||||
dependencies:
|
||||
- build
|
||||
- fetch
|
||||
image:
|
||||
name: goreleaser/goreleaser
|
||||
entrypoint: [ '' ]
|
||||
only:
|
||||
- tags
|
||||
variables:
|
||||
# Disable shallow cloning so that goreleaser can diff between tags to
|
||||
# generate a changelog.
|
||||
GIT_DEPTH: 0
|
||||
script:
|
||||
- sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
|
||||
- apk add --no-cache upx
|
||||
- goreleaser release --clean
|
||||
@@ -1,27 +1,38 @@
|
||||
version: 2
|
||||
project_name: panel
|
||||
|
||||
builds:
|
||||
- id: panel
|
||||
binary: panel
|
||||
- id: web
|
||||
main: ./cmd/web
|
||||
binary: web
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
ldflags:
|
||||
- -s -w --extldflags "-static"
|
||||
- id: cli
|
||||
main: ./cmd/cli
|
||||
binary: cli
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- GOPROXY=https://goproxy.cn,direct
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
goamd64:
|
||||
- v2
|
||||
ldflags:
|
||||
- -s -w --extldflags "-static"
|
||||
tags:
|
||||
- nomsgpack
|
||||
|
||||
upx:
|
||||
- enabled: true
|
||||
# Filter by build ID.
|
||||
ids: [ panel ]
|
||||
ids:
|
||||
- web
|
||||
- cli
|
||||
# Compress argument.
|
||||
# Valid options are from '1' (faster) to '9' (better), and 'best'.
|
||||
compress: best
|
||||
@@ -31,32 +42,14 @@ upx:
|
||||
brute: false
|
||||
|
||||
archives:
|
||||
- id: panel-archive
|
||||
- id: panel
|
||||
builds:
|
||||
- panel
|
||||
- web
|
||||
- cli
|
||||
format: zip
|
||||
wrap_in_directory: false
|
||||
strip_binary_directory: true
|
||||
files:
|
||||
- LICENSE
|
||||
- docs/*
|
||||
- public/*
|
||||
- config.example.yml
|
||||
- storage/*
|
||||
- lang/*
|
||||
- scripts/*
|
||||
- panel-example.conf
|
||||
|
||||
gitlab_urls:
|
||||
api: https://git.haozi.net/api/v4/
|
||||
download: https://git.haozi.net
|
||||
# set to true if you use a self-signed certificate
|
||||
skip_tls_verify: false
|
||||
# set to true if you want to upload to the Package Registry rather than attachments
|
||||
# Only works with GitLab 13.5+
|
||||
#
|
||||
# Since: v1.3
|
||||
use_package_registry: true
|
||||
# Set this if you set GITLAB_TOKEN to the value of CI_JOB_TOKEN.
|
||||
#
|
||||
# Since: v1.11
|
||||
use_job_token: true
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
[简体中文] | [<a href="README_EN.md">English</a>]
|
||||
</p>
|
||||
|
||||
<h1 align="center">耗子面板</h1>
|
||||
<h1 align="center" style="font-size: 40px">耗子面板</h1>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/10950" target="_blank"><img src="https://trendshift.io/api/badge/repositories/10950" alt="TheTNB%2Fpanel | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/TheTNB/panel/releases"><img src="https://img.shields.io/github/release/TheTNB/panel.svg"></a>
|
||||
@@ -14,71 +18,91 @@
|
||||
|
||||
耗子面板是使用 Golang + Vue 开发的开源轻量 Linux 服务器运维管理面板。
|
||||
|
||||
交流QQ群:[12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | 论坛:[tom.moe](https://tom.moe) | 赞助:[爱发电](https://afdian.net/a/TheTNB)
|
||||
QQ群:[12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | 微信群:[复制此链接](https://work.weixin.qq.com/gm/d8ebf618553398d454e3378695c858b6) | 论坛:[tom.moe](https://tom.moe) | 赞助:[爱发电](https://afdian.com/a/TheTNB)
|
||||
|
||||
## 优势
|
||||
|
||||
1. **极低占用:** 在 Debian 12 下部署面板 + LNMP 环境,内存占用不到 500 MB,遥遥领先于使用容器化的其他面板。
|
||||
1. **极低占用:** 在 Debian 下部署面板 + LNMP 环境,内存占用不到 500 MB,遥遥领先于使用容器化的其他面板。
|
||||
2. **低破坏性:** 面板的设计理念是尽可能减少对系统的额外修改,在同类面板中,我们对系统的修改最少。
|
||||
3. **追随时代:** 面板所有组件均走在时代前沿,更新快,功能强大,安全性有保障。
|
||||
4. **高效运维:** 面板 UI 界面简洁,操作简单,无需繁琐的配置,即可快速部署各类环境、调整应用设置。
|
||||
5. **离线运行:** 面板运行可不依赖任何外部服务,你甚至可以在部署完成后停止面板进程,不会对已部署服务造成任何影响。
|
||||
6. **久经考验:** 我们生产环境自 2022 年即开始使用,已稳定运行 1 年以上无事故。
|
||||
7. **开源开放:** 面板开源,你可以自由修改、审计面板源码,安全性有保障。
|
||||
4. **高效运维:** 面板界面简洁,操作简单,无需繁琐的配置,即可快速部署各类环境、调整应用设置。
|
||||
5. **离线运行:** 面板运行可不依赖任何外部服务,您甚至可以在部署完成后停止面板进程,不会对已部署服务造成任何影响。
|
||||
6. **久经考验:** 我们生产环境自 2022 年即开始使用,已稳定运行 2 年无事故。
|
||||
7. **开源开放:** 面板开源,您可以自由修改、审计面板源码,安全性有保障。
|
||||
|
||||
## UI 截图
|
||||
|
||||

|
||||

|
||||
|
||||
## 运行环境
|
||||
|
||||
耗子面板仅支持 `amd64` | `arm64` 架构下的主流系统的最新版本,不支持 `Ubuntu`,因为其发版太过频繁,难以维护。
|
||||
耗子面板支持 `amd64` | `arm64` 架构下的主流系统,下表中的系统均已测试 LNMP 环境安装。
|
||||
|
||||
低配机器建议使用 `Debian`,资源占用较 `RHEL` 系更低。其他机器建议使用 `AlmaLinux` | `RockyLinux`,维护周期更长也更稳定。
|
||||
优先建议使用标注**推荐**的系统,无特殊情况不建议使用标注**不推荐**的系统。
|
||||
|
||||
不在下表中的其他 RHEL 9.x 系统,可自行尝试安装,但不保证能够正常运行,且不提供技术支持(理论上不会有大问题)。
|
||||
不在下表中的其他系统,可自行尝试安装,但不提供技术支持(接受相关 PR 提交)。
|
||||
|
||||
| 系统 | 版本 |
|
||||
|------------|----|
|
||||
| RHEL | 9 |
|
||||
| AlmaLinux | 9 |
|
||||
| RockyLinux | 9 |
|
||||
| Debian | 12 |
|
||||
| 系统 | 版本 | 备注 |
|
||||
|---------------------|-----|-----|
|
||||
| AlmaLinux | 9 | 推荐 |
|
||||
| AlmaLinux | 8 | 不推荐 |
|
||||
| RockyLinux | 9 | 支持 |
|
||||
| RockyLinux | 8 | 不推荐 |
|
||||
| CentOS Stream | 9 | 不推荐 |
|
||||
| CentOS Stream | 8 | 不推荐 |
|
||||
| Ubuntu | 24 | 推荐 |
|
||||
| Ubuntu | 22 | 支持 |
|
||||
| Debian | 12 | 推荐 |
|
||||
| Debian | 11 | 支持 |
|
||||
| OpenCloudOS | 9 | 支持 |
|
||||
| TencentOS Server | 4 | 支持 |
|
||||
| TencentOS Server | 3.1 | 不推荐 |
|
||||
| Alibaba Cloud Linux | 3.2 | 不推荐 |
|
||||
| Anolis | 8 | 不推荐 |
|
||||
| openEuler | 22 | 不推荐 |
|
||||
|
||||
随着系统版本的不断更新,我们亦可能会终止部分过于老旧的系统的支持,以保证面板的稳定性。
|
||||
随着系统版本的不断更新,我们亦可能会终止部分过于老旧的系统的支持,以保证面板的健壮性。
|
||||
|
||||
## 安装面板
|
||||
|
||||
> **Warning**
|
||||
> 安装面板前,你需要了解 LNMP 环境的基本知识,以及如何处理常见的 LNMP 环境问题,不建议 0 基础的用户安装和使用耗子面板。
|
||||
> 安装面板前,您需要了解 LNMP 环境的基本知识,以及如何处理常见的 LNMP 环境问题,我们不建议 0 基础的用户安装和使用耗子面板。
|
||||
|
||||
如果你决定继续,请以`root`用户登录服务器,执行以下命令安装面板:
|
||||
如果您的服务器有未挂载的数据盘,可在安装前以`root`用户登录服务器运行以下命令自动挂载,面板安装后不支持跨目录迁移。
|
||||
|
||||
```shell
|
||||
HAOZI_DL_URL="https://dl.cdn.haozi.net/panel"; curl -sSL -O ${HAOZI_DL_URL}/install_panel.sh && curl -sSL -O ${HAOZI_DL_URL}/install_panel.sh.checksum.txt && sha256sum -c install_panel.sh.checksum.txt && bash install_panel.sh || echo "Checksum 验证失败,文件可能被篡改,已终止操作"
|
||||
curl -fsLm 10 -o auto_mount.sh https://dl.cdn.haozi.net/panel/auto_mount.sh && bash auto_mount.sh
|
||||
```
|
||||
|
||||
准备就绪后,请以`root`用户登录服务器,运行以下命令安装面板:
|
||||
|
||||
**当前 v2.3.x 为测试版本,欢迎帮助我们测试在不同操作系统下的兼容性。**
|
||||
|
||||
```shell
|
||||
curl -fsLm 10 -o install.sh https://dl.cdn.haozi.net/panel/install.sh && bash install.sh
|
||||
```
|
||||
|
||||
## 卸载面板
|
||||
|
||||
优先建议备份数据重装系统,这样可以保证系统纯净。
|
||||
|
||||
如果你无法重装系统,请以`root`用户登录服务器,执行以下命令卸载面板:
|
||||
如果您无法重装系统,请以`root`用户登录服务器,执行以下命令卸载面板:
|
||||
|
||||
```shell
|
||||
HAOZI_DL_URL="https://dl.cdn.haozi.net/panel"; curl -sSL -O ${HAOZI_DL_URL}/uninstall_panel.sh && curl -sSL -O ${HAOZI_DL_URL}/uninstall_panel.sh.checksum.txt && sha256sum -c uninstall_panel.sh.checksum.txt && bash uninstall_panel.sh || echo "Checksum 验证失败,文件可能被篡改,已终止操作"
|
||||
curl -fsLm 10 -o uninstall.sh https://dl.cdn.haozi.net/panel/uninstall.sh && bash uninstall.sh
|
||||
```
|
||||
|
||||
卸载面板前请务必备份好所有数据,提前卸载面板全部插件。卸载后数据将**无法恢复**!
|
||||
卸载面板前请务必备份好所有数据,提前卸载面板全部应用。卸载后数据将**无法恢复**!
|
||||
|
||||
## 日常维护
|
||||
|
||||
使用`panel`命令进行日常维护:
|
||||
使用`panel-cli`命令进行日常维护:
|
||||
|
||||
```shell
|
||||
panel
|
||||
panel-cli
|
||||
```
|
||||
|
||||
在 [Wiki](https://github.com/TheTNB/panel/wiki) 中查看更多使用方法和技巧。
|
||||
在 [文档](https://tom.moe/docs?category=57) 中查看更多使用方法和技巧。
|
||||
|
||||
## 问题反馈
|
||||
|
||||
@@ -88,21 +112,38 @@ panel
|
||||
|
||||
## 赞助商
|
||||
|
||||
如果耗子面板对你有帮助,欢迎[赞助我们](https://afdian.net/a/TheTNB),感谢以下支持者/赞助商的支持:
|
||||
如果耗子面板对您有帮助,欢迎[赞助我们](https://afdian.com/a/TheTNB),感谢以下支持者/赞助商的支持:
|
||||
|
||||
**同时接受云资源赞助,可通过QQ群咨询联系**
|
||||
|
||||
### 资金
|
||||
|
||||
<a href="https://www.weixiaoduo.com/">
|
||||
<img height="80" src=".github/assets/wxd.png" alt="微晓朵">
|
||||
</a>
|
||||
|
||||
### 服务器
|
||||
|
||||
- [微晓朵](https://www.weixiaoduo.com/)
|
||||
<a href="https://www.dkdun.cn/aff/MQZZNVHQ">
|
||||
<img height="80" src=".github/assets/dk.png" alt="林枫云">
|
||||
</a>
|
||||
|
||||
### CDN
|
||||
|
||||
- [无畏云加速](https://su.sctes.com/register?code=8st689ujpmm2p)
|
||||
- [盾云CDN](http://cdn.ddunyun.com/)
|
||||
<a href="https://su.sctes.com/register?code=8st689ujpmm2p">
|
||||
<img height="80" src=".github/assets/sctes.png" alt="无畏云加速">
|
||||
</a>
|
||||
<a href="https://su.sctes.com/register?code=8st689ujpmm2p">
|
||||
<img height="80" src=".github/assets/wafpro.png" alt="WAFPRO">
|
||||
</a>
|
||||
<a href="https://scdn.ddunyun.com/">
|
||||
<img height="80" src=".github/assets/ddunyun.png" alt="盾云SCDN">
|
||||
</a>
|
||||
|
||||
### 赞助商们
|
||||
|
||||
<p align="center">
|
||||
<a target="_blank" href="https://afdian.net/a/TheTNB">
|
||||
<a target="_blank" href="https://afdian.com/a/TheTNB">
|
||||
<img alt="sponsors" src="https://github.com/TheTNB/sponsor/blob/main/sponsors.svg?raw=true"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
[<a href="README.md">简体中文</a>] | [English]
|
||||
</p>
|
||||
|
||||
<h1 align="center">Rat Panel</h1>
|
||||
<h1 align="center" style="font-size: 40px">Rat Panel</h1>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/10950" target="_blank"><img src="https://trendshift.io/api/badge/repositories/10950" alt="TheTNB%2Fpanel | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/TheTNB/panel/releases"><img src="https://img.shields.io/github/release/TheTNB/panel.svg"></a>
|
||||
@@ -14,48 +18,68 @@
|
||||
|
||||
The Rat Panel is an open source lightweight Linux server operation and maintenance management panel developed using Golang and Vue.
|
||||
|
||||
Communication QQ group: [12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | Forum: [tom.moe](https://tom.moe) | Sponsor: [Open Collective](https://opencollective.com/tnb)
|
||||
QQ group: [12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | WeChat group: [Copy this link](https://work.weixin.qq.com/gm/d8ebf618553398d454e3378695c858b6) | Forum: [tom.moe](https://tom.moe) | Sponsor: [Open Collective](https://opencollective.com/tnb)
|
||||
|
||||
## Advantages
|
||||
|
||||
1. **Extremely low usage:** Deploying the panel + LNMP environment under Debian 12, the memory usage is less than 500 MB, far ahead of other panels using containerization.
|
||||
1. **Extremely low usage:** Deploying the panel + LNMP environment under Debian, the memory usage is less than 500 MB, far ahead of other panels using containerization.
|
||||
2. **Low destructiveness:** The design concept of the panel is to minimize the additional modifications to the system. Among similar panels, we have made the least modifications to the system.
|
||||
3. **Follow the times:** All components of the panel are at the forefront of the times, updated quickly, powerful functions, and guaranteed security.
|
||||
4. **Efficient operation and maintenance:** The panel UI interface is simple and easy to operate, and you can quickly deploy various environments and adjust application settings without complicated configuration.
|
||||
4. **Efficient operation and maintenance:** The panel is simple and easy to operate, and you can quickly deploy various environments and adjust application settings without complicated configuration.
|
||||
5. **Offline operation:** The panel can run without relying on any external services. You can even stop the panel process after deployment is complete, and it will not affect the deployed services.
|
||||
6. **Tested by time:** We have been using it in production environment since 2022, and it has been running stably for more than 1 year without any accidents.
|
||||
6. **Tested by time:** We have been using it in production environment since 2022, and it has been running stably for 2 year without any accidents.
|
||||
7. **Open source and open:** The panel is open source, you can freely modify and audit the panel source code, and the security is guaranteed.
|
||||
|
||||
## UI Screenshots
|
||||
|
||||

|
||||

|
||||
|
||||
## Operating Environment
|
||||
|
||||
Rat Panel only supports the latest version of mainstream systems under the `amd64` | `arm64` architecture. It does not support `Ubuntu` because its releases are too frequent and difficult to maintain.
|
||||
Rat Panel supports mainstream systems under the `amd64` | `arm64` architecture, and the systems in the table below have been tested for LNMP environment installation.
|
||||
|
||||
Recommended to use `Debian` for low-configuration machines, as its resource usage is lower than that of the `RHEL` system. For other machines, recommended to use `RockyLinux` | `AlmaLinux`, which has a longer maintenance cycle and is more stable.
|
||||
It is recommended to use the systems marked **Recommended** first, and not recommended to use the systems marked **Not recommended** without special circumstances.
|
||||
|
||||
For other RHEL 9.x systems not in the table, you can try to install it yourself, but normal operation is not guaranteed, and technical support is not provided (theoretically there will be no major question).
|
||||
For other systems not listed in the table below, you can try to install them by yourself, but there is no technical support will be provided (accept related PR submissions).
|
||||
|
||||
| OS | Version |
|
||||
|------------|---------|
|
||||
| RHEL | 9 |
|
||||
| RockyLinux | 9 |
|
||||
| AlmaLinux | 9 |
|
||||
| Debian | 12 |
|
||||
| OS | Version | Note |
|
||||
|---------------------|---------|-----------------|
|
||||
| AlmaLinux | 9 | Recommended |
|
||||
| AlmaLinux | 8 | Not recommended |
|
||||
| RockyLinux | 9 | Support |
|
||||
| RockyLinux | 8 | Not recommended |
|
||||
| CentOS Stream | 9 | Not recommended |
|
||||
| CentOS Stream | 8 | Not recommended |
|
||||
| Ubuntu | 24 | Recommended |
|
||||
| Ubuntu | 22 | Support |
|
||||
| Debian | 12 | Recommended |
|
||||
| Debian | 11 | Support |
|
||||
| OpenCloudOS | 9 | Support |
|
||||
| TencentOS Server | 4 | Support |
|
||||
| TencentOS Server | 3.1 | Not recommended |
|
||||
| Alibaba Cloud Linux | 3.2 | Not recommended |
|
||||
| Anolis | 8 | Not recommended |
|
||||
| openEuler | 22 | Not recommended |
|
||||
|
||||
As system versions are constantly updated, we may also terminate support for some older systems to ensure the stability of the panel.
|
||||
As system versions are constantly updated, we may also terminate support for some older systems to ensure the robustness of the panel.
|
||||
|
||||
## Install Panel
|
||||
|
||||
> **Warning**
|
||||
> Before installing the panel, you need to understand the basic knowledge of the LNMP environment and how to deal with common LNMP environment problems. It is not recommended for users with zero basic knowledge to install and use Rat Panel.
|
||||
> Before installing the panel, you need to understand the basic knowledge of the LNMP environment and how to deal with common LNMP environment problems. We are not recommended for users with zero basic knowledge to install and use Rat Panel.
|
||||
|
||||
If you decide to continue, please log in to the server as `root` user and execute the following command to install the panel:
|
||||
If your server has an unmounted data disk, you can run the following command as the `root` user to automatically mount it before installation. The panel does not support cross-directory migration after installation.
|
||||
|
||||
```shell
|
||||
HAOZI_DL_URL="https://dl.cdn.haozi.net/panel"; curl -sSL -O ${HAOZI_DL_URL}/install_panel.sh && curl -sSL -O ${HAOZI_DL_URL}/install_panel.sh.checksum.txt && sha256sum -c install_panel.sh.checksum.txt && bash install_panel.sh || echo "Checksum Verification Failed, File May Have Been Tampered With, Operation Terminated"
|
||||
curl -fsLm 10 -o auto_mount.sh https://dl.cdn.haozi.net/panel/auto_mount.sh && bash auto_mount.sh
|
||||
```
|
||||
|
||||
After you are ready, log in to the server as the `root` user and run the following command to install the panel:
|
||||
|
||||
**Current v2.3.x is a test version, welcome to help us test compatibility on different operating systems.**
|
||||
|
||||
```shell
|
||||
curl -fsLm 10 -o install.sh https://dl.cdn.haozi.net/panel/install.sh && bash install.sh
|
||||
```
|
||||
|
||||
## Uninstall Panel
|
||||
@@ -65,20 +89,20 @@ Recommended to back up data and reinstall the system first, so that the system c
|
||||
If you are unable to reinstall the system, log in to the server as the `root` user and execute the following command to uninstall the panel:
|
||||
|
||||
```shell
|
||||
HAOZI_DL_URL="https://dl.cdn.haozi.net/panel"; curl -sSL -O ${HAOZI_DL_URL}/uninstall_panel.sh && curl -sSL -O ${HAOZI_DL_URL}/uninstall_panel.sh.checksum.txt && sha256sum -c uninstall_panel.sh.checksum.txt && bash uninstall_panel.sh || echo "Checksum Verification Failed, File May Have Been Tampered With, Operation Terminated"
|
||||
curl -fsLm 10 -o uninstall.sh https://dl.cdn.haozi.net/panel/uninstall.sh && bash uninstall.sh
|
||||
```
|
||||
|
||||
Before uninstalling the panel, please be sure to back up all data and uninstall all panel plugins in advance. The data will **not be recoverable** after uninstallation!
|
||||
|
||||
## Daily Maintenance
|
||||
|
||||
Use `panel` command for daily maintenance:
|
||||
Use `panel-cli` command for daily maintenance:
|
||||
|
||||
```shell
|
||||
panel
|
||||
panel-cli
|
||||
```
|
||||
|
||||
See more usage methods and tips in [Wiki](https://github.com/TheTNB/panel/wiki).
|
||||
See more usage methods and tips in [Document](https://tom.moe/docs?category=57).
|
||||
|
||||
## Feedback
|
||||
|
||||
@@ -92,17 +116,34 @@ If the Rat Panel is helpful to you, welcome to [sponsor us](https://opencollecti
|
||||
|
||||
**Accept cloud resources and financial sponsorship, you can contact us through QQ group**
|
||||
|
||||
### Financial
|
||||
|
||||
<a href="https://www.weixiaoduo.com/">
|
||||
<img height="80" src=".github/assets/wxd.png" alt="微晓朵">
|
||||
</a>
|
||||
|
||||
### Server
|
||||
|
||||
- [微晓朵](https://www.weixiaoduo.com/)
|
||||
<a href="https://www.dkdun.cn/aff/MQZZNVHQ">
|
||||
<img height="80" src=".github/assets/dk.png" alt="林枫云">
|
||||
</a>
|
||||
|
||||
### CDN
|
||||
|
||||
- [无畏云加速](https://su.sctes.com/register?code=8st689ujpmm2p)
|
||||
- [盾云CDN](http://cdn.ddunyun.com/)
|
||||
<a href="https://su.sctes.com/register?code=8st689ujpmm2p">
|
||||
<img height="80" src=".github/assets/sctes.png" alt="无畏云加速">
|
||||
</a>
|
||||
<a href="https://su.sctes.com/register?code=8st689ujpmm2p">
|
||||
<img height="80" src=".github/assets/wafpro.png" alt="WAFPRO">
|
||||
</a>
|
||||
<a href="https://scdn.ddunyun.com/">
|
||||
<img height="80" src=".github/assets/ddunyun.png" alt="盾云SCDN">
|
||||
</a>
|
||||
|
||||
### Sponsors
|
||||
|
||||
<p align="center">
|
||||
<a target="_blank" href="https://afdian.net/a/TheTNB">
|
||||
<a target="_blank" href="https://afdian.com/a/TheTNB">
|
||||
<img alt="sponsors" src="https://github.com/TheTNB/sponsor/blob/main/sponsors.svg?raw=true"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
## 安全说明
|
||||
|
||||
安全性是我们最关心的问题之一,我们已在多个不同应用的生产环境广泛应用耗子面板,至今无一例安全事故。
|
||||
**我们不对面板的安全性做任何保证**,但安全性是我们最关心的问题之一,我们已在多个不同应用的生产环境广泛应用耗子面板,至今无任何安全事故。
|
||||
|
||||
耗子面板采用业界多种方案尽可能保证面板的安全性,但是我们不能保证面板的绝对安全性,**因此我们不对面板的安全性做任何保证**。
|
||||
如果您在面板中发现任何安全问题,请勿提交 Issue,可通过以下方式直接联系我们:
|
||||
|
||||
如果您在使用面板的过程中发现任何安全问题,请勿提交 Issue,可通过以下方式直接联系我们:
|
||||
|
||||
- 邮箱:i@haozi.net
|
||||
- 邮箱:[admin@haozi.net](mailto:admin@haozi.net)
|
||||
- QQ:826896000
|
||||
|
||||
致某些 CVE 爱好者:通过面板 `access_token` 执行的任何操作(包括且不限于:获取 root 权限、读取/写入系统敏感文件、执行任意 shell 命令等)均不被认为是安全问题,请不要刷此类报告浪费彼此的时间,这类低水平的报告对你的简历也没有任何帮助。
|
||||
提前感谢您的支持与帮助!
|
||||
|
||||
致某些安全初学者:通过面板 `session` / `access_token` 执行的任何操作(包括且不限于:获取 root 权限、读取/写入系统敏感文件、执行任意 shell 命令等)均不被认为是安全问题,请不要刷此类报告浪费彼此的时间,这类低水平的报告对你的简历也没有任何帮助。
|
||||
|
||||
## Security Policy
|
||||
|
||||
Security is one of our biggest concerns. We have widely used Rat Panel in production environments for many different applications, and there has been no safety incident so far.
|
||||
|
||||
Rat Panel adopts various solutions in the industry to ensure the security of the panel as much as possible, but we cannot guarantee the absolute security of the panel, **therefore we do not make any guarantees on the security of the panel**.
|
||||
**We do not make any guarantees about the security of the panel**, but security is one of our top concerns. We have widely used the Rat panel in the production environment of multiple different applications, and there has been no security incidents to date.
|
||||
|
||||
If you find any security issues while using the panel, please do not submit an Issue. You can contact us directly through the following methods:
|
||||
|
||||
- Email: i@haozi.net
|
||||
- Email: [admin@haozi.net](mailto:admin@haozi.net)
|
||||
- Telegram: @devhaozi
|
||||
|
||||
To some CVE enthusiasts: Any operation performed through the panel `access_token` (including but not limited to: obtaining root permissions, reading/writing system sensitive files, executing arbitrary shell commands, etc.) is not considered a security issue, please Don’t waste each other’s time with these types of reports. These low-level reports won’t do anything to help your resume.
|
||||
Thank you in advance for your support and help!
|
||||
|
||||
To some security beginners: Any operation performed through the panel `session` / `access_token` (including but not limited to: obtaining root permissions, reading/writing sensitive system files, executing arbitrary shell commands, etc.) is not considered a security issue. Please do not waste each other's time by submitting such reports. Such low-level reports are of no help to your resume.
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/goravel/framework/contracts/console"
|
||||
"github.com/goravel/framework/contracts/console/command"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
|
||||
"github.com/TheTNB/panel/app/models"
|
||||
"github.com/TheTNB/panel/internal/services"
|
||||
panelcert "github.com/TheTNB/panel/pkg/cert"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
// CertRenew 证书续签
|
||||
type CertRenew struct {
|
||||
}
|
||||
|
||||
// Signature The name and signature of the console command.
|
||||
func (receiver *CertRenew) Signature() string {
|
||||
return "panel:cert-renew"
|
||||
}
|
||||
|
||||
// Description The console command description.
|
||||
func (receiver *CertRenew) Description() string {
|
||||
ctx := context.Background()
|
||||
return facades.Lang(ctx).Get("commands.panel:cert-renew.description")
|
||||
}
|
||||
|
||||
// Extend The console command extend.
|
||||
func (receiver *CertRenew) Extend() command.Extend {
|
||||
return command.Extend{
|
||||
Category: "panel",
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Execute the console command.
|
||||
func (receiver *CertRenew) Handle(console.Context) error {
|
||||
if types.Status != types.StatusNormal {
|
||||
return nil
|
||||
}
|
||||
|
||||
var certs []models.Cert
|
||||
err := facades.Orm().Query().With("Website").With("User").With("DNS").Find(&certs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, cert := range certs {
|
||||
if !cert.AutoRenew {
|
||||
continue
|
||||
}
|
||||
|
||||
decode, err := panelcert.ParseCert(cert.Cert)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 结束时间大于 7 天的证书不续签
|
||||
endTime := carbon.FromStdTime(decode.NotAfter)
|
||||
if endTime.Gt(carbon.Now().AddDays(7)) {
|
||||
continue
|
||||
}
|
||||
|
||||
certService := services.NewCertImpl()
|
||||
_, err = certService.Renew(cert.ID)
|
||||
if err != nil {
|
||||
facades.Log().Tags("面板", "证书管理").With(map[string]any{
|
||||
"cert_id": cert.ID,
|
||||
"error": err.Error(),
|
||||
}).Infof("证书续签失败")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/goravel/framework/contracts/console"
|
||||
"github.com/goravel/framework/contracts/console/command"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
"github.com/goravel/framework/support/color"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/TheTNB/panel/app/models"
|
||||
"github.com/TheTNB/panel/internal/services"
|
||||
"github.com/TheTNB/panel/pkg/tools"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
// Monitoring 系统监控
|
||||
type Monitoring struct {
|
||||
}
|
||||
|
||||
// Signature The name and signature of the console command.
|
||||
func (receiver *Monitoring) Signature() string {
|
||||
return "panel:monitoring"
|
||||
}
|
||||
|
||||
// Description The console command description.
|
||||
func (receiver *Monitoring) Description() string {
|
||||
ctx := context.Background()
|
||||
return facades.Lang(ctx).Get("commands.panel:monitoring.description")
|
||||
}
|
||||
|
||||
// Extend The console command extend.
|
||||
func (receiver *Monitoring) Extend() command.Extend {
|
||||
return command.Extend{
|
||||
Category: "panel",
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Execute the console command.
|
||||
func (receiver *Monitoring) Handle(console.Context) error {
|
||||
if types.Status != types.StatusNormal {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 将等待中的任务分发
|
||||
// TODO 有bug,需要设计一个锁机制防止重复分发
|
||||
task := services.NewTaskImpl()
|
||||
_ = task.DispatchWaiting()
|
||||
|
||||
setting := services.NewSettingImpl()
|
||||
monitor := setting.Get(models.SettingKeyMonitor)
|
||||
if !cast.ToBool(monitor) {
|
||||
return nil
|
||||
}
|
||||
|
||||
info := tools.GetMonitoringInfo()
|
||||
translate := facades.Lang(context.Background())
|
||||
|
||||
// 去除部分数据以减少数据库存储
|
||||
info.Disk = nil
|
||||
info.Cpus = nil
|
||||
|
||||
if types.Status != types.StatusNormal {
|
||||
return nil
|
||||
}
|
||||
err := facades.Orm().Query().Create(&models.Monitor{
|
||||
Info: info,
|
||||
})
|
||||
if err != nil {
|
||||
facades.Log().Tags("面板", "系统监控").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Infof("保存失败")
|
||||
color.Red().Printfln(translate.Get("commands.panel:monitoring.fail")+": %s", err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
// 删除过期数据
|
||||
days := cast.ToInt(setting.Get(models.SettingKeyMonitorDays))
|
||||
if days <= 0 || types.Status != types.StatusNormal {
|
||||
return nil
|
||||
}
|
||||
if _, err = facades.Orm().Query().Where("created_at < ?", carbon.Now().SubDays(days).ToDateTimeString()).Delete(&models.Monitor{}); err != nil {
|
||||
facades.Log().Tags("面板", "系统监控").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Infof("删除过期数据失败")
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,733 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/console"
|
||||
"github.com/goravel/framework/contracts/console/command"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
"github.com/goravel/framework/support/color"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/TheTNB/panel/app/models"
|
||||
"github.com/TheTNB/panel/internal/services"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
"github.com/TheTNB/panel/pkg/tools"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
// Panel 面板命令行
|
||||
type Panel struct {
|
||||
}
|
||||
|
||||
// Signature The name and signature of the console command.
|
||||
func (receiver *Panel) Signature() string {
|
||||
return "panel"
|
||||
}
|
||||
|
||||
// Description The console command description.
|
||||
func (receiver *Panel) Description() string {
|
||||
ctx := context.Background()
|
||||
return facades.Lang(ctx).Get("commands.panel.description")
|
||||
}
|
||||
|
||||
// Extend The console command extend.
|
||||
func (receiver *Panel) Extend() command.Extend {
|
||||
return command.Extend{
|
||||
Category: "panel",
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Execute the console command.
|
||||
func (receiver *Panel) Handle(ctx console.Context) error {
|
||||
action := ctx.Argument(0)
|
||||
arg1 := ctx.Argument(1)
|
||||
arg2 := ctx.Argument(2)
|
||||
arg3 := ctx.Argument(3)
|
||||
arg4 := ctx.Argument(4)
|
||||
arg5 := ctx.Argument(5)
|
||||
|
||||
translate := facades.Lang(context.Background())
|
||||
|
||||
switch action {
|
||||
case "init":
|
||||
var check models.User
|
||||
err := facades.Orm().Query().FirstOrFail(&check)
|
||||
if err == nil {
|
||||
color.Red().Printfln(translate.Get("commands.panel.init.exist"))
|
||||
return nil
|
||||
}
|
||||
|
||||
settings := []models.Setting{{Key: models.SettingKeyName, Value: "耗子面板"}, {Key: models.SettingKeyMonitor, Value: "1"}, {Key: models.SettingKeyMonitorDays, Value: "30"}, {Key: models.SettingKeyBackupPath, Value: "/www/backup"}, {Key: models.SettingKeyWebsitePath, Value: "/www/wwwroot"}, {Key: models.SettingKeyVersion, Value: facades.Config().GetString("panel.version")}}
|
||||
err = facades.Orm().Query().Create(&settings)
|
||||
if err != nil {
|
||||
color.Red().Printfln(translate.Get("commands.panel.init.fail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
hash, err := facades.Hash().Make(str.RandomString(32))
|
||||
if err != nil {
|
||||
color.Red().Printfln(translate.Get("commands.panel.init.fail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
user := services.NewUserImpl()
|
||||
_, err = user.Create("admin", hash)
|
||||
if err != nil {
|
||||
color.Red().Printfln(translate.Get("commands.panel.init.adminFail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Green().Printfln(translate.Get("commands.panel.init.success"))
|
||||
|
||||
case "update":
|
||||
var task models.Task
|
||||
if err := facades.Orm().Query().Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).FirstOrFail(&task); err == nil {
|
||||
color.Red().Printfln(translate.Get("commands.panel.update.taskCheck"))
|
||||
return nil
|
||||
}
|
||||
if _, err := facades.Orm().Query().Exec("PRAGMA wal_checkpoint(TRUNCATE)"); err != nil {
|
||||
types.Status = types.StatusFailed
|
||||
color.Red().Printfln(translate.Get("commands.panel.update.dbFail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
panel, err := tools.GetLatestPanelVersion()
|
||||
if err != nil {
|
||||
color.Red().Printfln(translate.Get("commands.panel.update.versionFail"))
|
||||
return err
|
||||
}
|
||||
|
||||
// 停止面板服务,因为在shell中运行的和systemd的不同
|
||||
_ = systemctl.Stop("panel")
|
||||
|
||||
types.Status = types.StatusUpgrade
|
||||
if err = tools.UpdatePanel(panel); err != nil {
|
||||
types.Status = types.StatusFailed
|
||||
color.Red().Printfln(translate.Get("commands.panel.update.fail") + ": " + err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
types.Status = types.StatusNormal
|
||||
color.Green().Printfln(translate.Get("commands.panel.update.success"))
|
||||
tools.RestartPanel()
|
||||
|
||||
case "getInfo":
|
||||
var user models.User
|
||||
err := facades.Orm().Query().Where("id", 1).FirstOrFail(&user)
|
||||
if err != nil {
|
||||
color.Red().Printfln(translate.Get("commands.panel.getInfo.adminGetFail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
password := str.RandomString(16)
|
||||
hash, err := facades.Hash().Make(password)
|
||||
if err != nil {
|
||||
color.Red().Printfln(translate.Get("commands.panel.getInfo.passwordGenerationFail"))
|
||||
return nil
|
||||
}
|
||||
user.Username = str.RandomString(8)
|
||||
user.Password = hash
|
||||
if user.Email == "" {
|
||||
user.Email = str.RandomString(8) + "@example.com"
|
||||
}
|
||||
|
||||
err = facades.Orm().Query().Save(&user)
|
||||
if err != nil {
|
||||
color.Red().Printfln(translate.Get("commands.panel.getInfo.adminSaveFail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
port, err := shell.Execf(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`)
|
||||
if err != nil {
|
||||
color.Red().Printfln(translate.Get("commands.panel.portFail"))
|
||||
return nil
|
||||
}
|
||||
ip, err := tools.GetPublicIP()
|
||||
if err != nil {
|
||||
ip = "127.0.0.1"
|
||||
}
|
||||
protocol := "http"
|
||||
if facades.Config().GetBool("panel.ssl") {
|
||||
protocol = "https"
|
||||
}
|
||||
|
||||
color.Green().Printfln(translate.Get("commands.panel.getInfo.username") + ": " + user.Username)
|
||||
color.Green().Printfln(translate.Get("commands.panel.getInfo.password") + ": " + password)
|
||||
color.Green().Printfln(translate.Get("commands.panel.port") + ": " + port)
|
||||
color.Green().Printfln(translate.Get("commands.panel.entrance") + ": " + facades.Config().GetString("http.entrance"))
|
||||
color.Green().Printfln(translate.Get("commands.panel.getInfo.address") + ": " + protocol + "://" + ip + ":" + port + facades.Config().GetString("http.entrance"))
|
||||
|
||||
case "getPort":
|
||||
port, err := shell.Execf(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`)
|
||||
if err != nil {
|
||||
color.Red().Printfln(translate.Get("commands.panel.portFail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Green().Printfln(translate.Get("commands.panel.port") + ": " + port)
|
||||
|
||||
case "getEntrance":
|
||||
color.Green().Printfln(translate.Get("commands.panel.entrance") + ": " + facades.Config().GetString("http.entrance"))
|
||||
|
||||
case "deleteEntrance":
|
||||
oldEntrance, err := shell.Execf(`cat /www/panel/panel.conf | grep APP_ENTRANCE | awk -F '=' '{print $2}' | tr -d '\n'`)
|
||||
if err != nil {
|
||||
color.Red().Printfln(translate.Get("commands.panel.deleteEntrance.fail"))
|
||||
return nil
|
||||
}
|
||||
if _, err = shell.Execf("sed -i 's!APP_ENTRANCE=" + oldEntrance + "!APP_ENTRANCE=/!g' /www/panel/panel.conf"); err != nil {
|
||||
color.Red().Printfln(translate.Get("commands.panel.deleteEntrance.fail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Green().Printfln(translate.Get("commands.panel.deleteEntrance.success"))
|
||||
|
||||
case "writePlugin":
|
||||
slug := arg1
|
||||
version := arg2
|
||||
if len(slug) == 0 || len(version) == 0 {
|
||||
color.Red().Printfln(translate.Get("commands.panel.writePlugin.paramFail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
var plugin models.Plugin
|
||||
err := facades.Orm().Query().UpdateOrCreate(&plugin, models.Plugin{
|
||||
Slug: slug,
|
||||
}, models.Plugin{
|
||||
Version: version,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
color.Red().Printfln(translate.Get("commands.panel.writePlugin.fail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Green().Printfln(translate.Get("commands.panel.writePlugin.success"))
|
||||
|
||||
case "deletePlugin":
|
||||
slug := arg1
|
||||
if len(slug) == 0 {
|
||||
color.Red().Printfln(translate.Get("commands.panel.deletePlugin.paramFail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := facades.Orm().Query().Where("slug", slug).Delete(&models.Plugin{})
|
||||
if err != nil {
|
||||
color.Red().Printfln(translate.Get("commands.panel.deletePlugin.fail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Green().Printfln(translate.Get("commands.panel.deletePlugin.success"))
|
||||
|
||||
case "writeMysqlPassword":
|
||||
password := arg1
|
||||
if len(password) == 0 {
|
||||
color.Red().Printfln(translate.Get("commands.panel.writeMysqlPassword.paramFail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
var setting models.Setting
|
||||
err := facades.Orm().Query().UpdateOrCreate(&setting, models.Setting{
|
||||
Key: models.SettingKeyMysqlRootPassword,
|
||||
}, models.Setting{
|
||||
Value: password,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
color.Red().Printfln(translate.Get("commands.panel.writeMysqlPassword.fail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Green().Printfln(translate.Get("commands.panel.writeMysqlPassword.success"))
|
||||
|
||||
case "cleanTask":
|
||||
_, err := facades.Orm().Query().Model(&models.Task{}).Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).Update("status", models.TaskStatusFailed)
|
||||
if err != nil {
|
||||
color.Red().Printfln(translate.Get("commands.panel.cleanTask.fail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Green().Printfln(translate.Get("commands.panel.cleanTask.success"))
|
||||
|
||||
case "backup":
|
||||
backupType := arg1
|
||||
name := arg2
|
||||
path := arg3
|
||||
save := arg4
|
||||
hr := `+----------------------------------------------------`
|
||||
if len(backupType) == 0 || len(name) == 0 || len(path) == 0 || len(save) == 0 {
|
||||
color.Red().Printfln(translate.Get("commands.panel.backup.paramFail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Green().Printfln(hr)
|
||||
color.Green().Printfln("★ " + translate.Get("commands.panel.backup.start") + " [" + carbon.Now().ToDateTimeString() + "]")
|
||||
color.Green().Printfln(hr)
|
||||
|
||||
if !io.Exists(path) {
|
||||
if err := io.Mkdir(path, 0644); err != nil {
|
||||
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.backupDirFail") + ": " + err.Error())
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
switch backupType {
|
||||
case "website":
|
||||
color.Yellow().Printfln("|-" + translate.Get("commands.panel.backup.targetSite") + ": " + name)
|
||||
var website models.Website
|
||||
if err := facades.Orm().Query().Where("name", name).FirstOrFail(&website); err != nil {
|
||||
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.siteNotExist"))
|
||||
color.Green().Printfln(hr)
|
||||
return nil
|
||||
}
|
||||
|
||||
backupFile := path + "/" + website.Name + "_" + carbon.Now().ToShortDateTimeString() + ".zip"
|
||||
if _, err := shell.Execf(`cd '` + website.Path + `' && zip -r '` + backupFile + `' .`); err != nil {
|
||||
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.backupFail") + ": " + err.Error())
|
||||
return nil
|
||||
}
|
||||
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.backupSuccess"))
|
||||
|
||||
case "mysql":
|
||||
rootPassword := services.NewSettingImpl().Get(models.SettingKeyMysqlRootPassword)
|
||||
backupFile := name + "_" + carbon.Now().ToShortDateTimeString() + ".sql"
|
||||
|
||||
err := os.Setenv("MYSQL_PWD", rootPassword)
|
||||
if err != nil {
|
||||
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.mysqlBackupFail") + ": " + err.Error())
|
||||
color.Green().Printfln(hr)
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.targetMysql") + ": " + name)
|
||||
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.startExport"))
|
||||
if _, err = shell.Execf(`mysqldump -uroot ` + name + ` > /tmp/` + backupFile + ` 2>&1`); err != nil {
|
||||
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.exportFail") + ": " + err.Error())
|
||||
return nil
|
||||
}
|
||||
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.exportSuccess"))
|
||||
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.startCompress"))
|
||||
if _, err = shell.Execf("cd /tmp && zip -r " + backupFile + ".zip " + backupFile); err != nil {
|
||||
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.compressFail") + ": " + err.Error())
|
||||
return nil
|
||||
}
|
||||
if err := io.Remove("/tmp/" + backupFile); err != nil {
|
||||
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.deleteFail") + ": " + err.Error())
|
||||
return nil
|
||||
}
|
||||
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.compressSuccess"))
|
||||
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.startMove"))
|
||||
if err := io.Mv("/tmp/"+backupFile+".zip", path+"/"+backupFile+".zip"); err != nil {
|
||||
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.moveFail") + ": " + err.Error())
|
||||
return nil
|
||||
}
|
||||
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.moveSuccess"))
|
||||
_ = os.Unsetenv("MYSQL_PWD")
|
||||
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.success"))
|
||||
|
||||
case "postgresql":
|
||||
backupFile := name + "_" + carbon.Now().ToShortDateTimeString() + ".sql"
|
||||
check, err := shell.Execf(`su - postgres -c "psql -l" 2>&1`)
|
||||
if err != nil {
|
||||
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.databaseGetFail") + ": " + err.Error())
|
||||
color.Green().Printfln(hr)
|
||||
return nil
|
||||
}
|
||||
if !strings.Contains(check, name) {
|
||||
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.databaseNotExist"))
|
||||
color.Green().Printfln(hr)
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.targetPostgres") + ": " + name)
|
||||
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.startExport"))
|
||||
if _, err = shell.Execf(`su - postgres -c "pg_dump '` + name + `'" > /tmp/` + backupFile + ` 2>&1`); err != nil {
|
||||
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.exportFail") + ": " + err.Error())
|
||||
return nil
|
||||
}
|
||||
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.exportSuccess"))
|
||||
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.startCompress"))
|
||||
if _, err = shell.Execf("cd /tmp && zip -r " + backupFile + ".zip " + backupFile); err != nil {
|
||||
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.compressFail") + ": " + err.Error())
|
||||
return nil
|
||||
}
|
||||
if err := io.Remove("/tmp/" + backupFile); err != nil {
|
||||
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.deleteFail") + ": " + err.Error())
|
||||
return nil
|
||||
}
|
||||
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.compressSuccess"))
|
||||
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.startMove"))
|
||||
if err := io.Mv("/tmp/"+backupFile+".zip", path+"/"+backupFile+".zip"); err != nil {
|
||||
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.moveFail") + ": " + err.Error())
|
||||
return nil
|
||||
}
|
||||
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.moveSuccess"))
|
||||
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.success"))
|
||||
}
|
||||
|
||||
color.Green().Printfln(hr)
|
||||
files, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.cleanupFail") + ": " + err.Error())
|
||||
return nil
|
||||
}
|
||||
var filteredFiles []os.FileInfo
|
||||
for _, file := range files {
|
||||
if strings.HasPrefix(file.Name(), name) && strings.HasSuffix(file.Name(), ".zip") {
|
||||
fileInfo, err := os.Stat(filepath.Join(path, file.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
filteredFiles = append(filteredFiles, fileInfo)
|
||||
}
|
||||
}
|
||||
sort.Slice(filteredFiles, func(i, j int) bool {
|
||||
return filteredFiles[i].ModTime().After(filteredFiles[j].ModTime())
|
||||
})
|
||||
for i := cast.ToInt(save); i < len(filteredFiles); i++ {
|
||||
fileToDelete := filepath.Join(path, filteredFiles[i].Name())
|
||||
color.Yellow().Printfln("|-" + translate.Get("commands.panel.backup.cleanBackup") + ": " + fileToDelete)
|
||||
if err := io.Remove(fileToDelete); err != nil {
|
||||
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.cleanupFail") + ": " + err.Error())
|
||||
return nil
|
||||
}
|
||||
}
|
||||
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.cleanupSuccess"))
|
||||
color.Green().Printfln(hr)
|
||||
color.Green().Printfln("☆ " + translate.Get("commands.panel.backup.success") + " [" + carbon.Now().ToDateTimeString() + "]")
|
||||
color.Green().Printfln(hr)
|
||||
|
||||
case "cutoff":
|
||||
name := arg1
|
||||
save := arg2
|
||||
hr := `+----------------------------------------------------`
|
||||
if len(name) == 0 || len(save) == 0 {
|
||||
color.Red().Printfln(translate.Get("commands.panel.cutoff.paramFail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Green().Printfln(hr)
|
||||
color.Green().Printfln("★ " + translate.Get("commands.panel.cutoff.start") + " [" + carbon.Now().ToDateTimeString() + "]")
|
||||
color.Green().Printfln(hr)
|
||||
|
||||
color.Yellow().Printfln("|-" + translate.Get("commands.panel.cutoff.targetSite") + ": " + name)
|
||||
var website models.Website
|
||||
if err := facades.Orm().Query().Where("name", name).FirstOrFail(&website); err != nil {
|
||||
color.Red().Printfln("|-" + translate.Get("commands.panel.cutoff.siteNotExist"))
|
||||
color.Green().Printfln(hr)
|
||||
return nil
|
||||
}
|
||||
|
||||
logPath := "/www/wwwlogs/" + website.Name + ".log"
|
||||
if !io.Exists(logPath) {
|
||||
color.Red().Printfln("|-" + translate.Get("commands.panel.cutoff.logNotExist"))
|
||||
color.Green().Printfln(hr)
|
||||
return nil
|
||||
}
|
||||
|
||||
backupPath := "/www/wwwlogs/" + website.Name + "_" + carbon.Now().ToShortDateTimeString() + ".log.zip"
|
||||
if _, err := shell.Execf(`cd /www/wwwlogs && zip -r ` + backupPath + ` ` + website.Name + ".log"); err != nil {
|
||||
color.Red().Printfln("|-" + translate.Get("commands.panel.cutoff.backupFail") + ": " + err.Error())
|
||||
return nil
|
||||
}
|
||||
if _, err := shell.Execf(`echo "" > ` + logPath); err != nil {
|
||||
color.Red().Printfln("|-" + translate.Get("commands.panel.cutoff.clearFail") + ": " + err.Error())
|
||||
return nil
|
||||
}
|
||||
color.Green().Printfln("|-" + translate.Get("commands.panel.cutoff.cutSuccess"))
|
||||
|
||||
color.Green().Printfln(hr)
|
||||
files, err := os.ReadDir("/www/wwwlogs")
|
||||
if err != nil {
|
||||
color.Red().Printfln("|-" + translate.Get("commands.panel.cutoff.cleanupFail") + ": " + err.Error())
|
||||
return nil
|
||||
}
|
||||
var filteredFiles []os.FileInfo
|
||||
for _, file := range files {
|
||||
if strings.HasPrefix(file.Name(), website.Name) && strings.HasSuffix(file.Name(), ".log.zip") {
|
||||
fileInfo, err := os.Stat(filepath.Join("/www/wwwlogs", file.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
filteredFiles = append(filteredFiles, fileInfo)
|
||||
}
|
||||
}
|
||||
sort.Slice(filteredFiles, func(i, j int) bool {
|
||||
return filteredFiles[i].ModTime().After(filteredFiles[j].ModTime())
|
||||
})
|
||||
for i := cast.ToInt(save); i < len(filteredFiles); i++ {
|
||||
fileToDelete := filepath.Join("/www/wwwlogs", filteredFiles[i].Name())
|
||||
color.Yellow().Printfln("|-" + translate.Get("commands.panel.cutoff.clearLog") + ": " + fileToDelete)
|
||||
if err := io.Remove(fileToDelete); err != nil {
|
||||
color.Red().Printfln("|-" + translate.Get("commands.panel.cutoff.cleanupFail") + ": " + err.Error())
|
||||
return nil
|
||||
}
|
||||
}
|
||||
color.Green().Printfln("|-" + translate.Get("commands.panel.cutoff.cleanupSuccess"))
|
||||
color.Green().Printfln(hr)
|
||||
color.Green().Printfln("☆ " + translate.Get("commands.panel.cutoff.end") + " [" + carbon.Now().ToDateTimeString() + "]")
|
||||
color.Green().Printfln(hr)
|
||||
|
||||
case "writeSite":
|
||||
name := arg1
|
||||
status := cast.ToBool(arg2)
|
||||
path := arg3
|
||||
php := cast.ToInt(arg4)
|
||||
ssl := cast.ToBool(ctx.Argument(5))
|
||||
if len(name) == 0 || len(path) == 0 {
|
||||
color.Red().Printfln(translate.Get("commands.panel.writeSite.paramFail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
var website models.Website
|
||||
if err := facades.Orm().Query().Where("name", name).FirstOrFail(&website); err == nil {
|
||||
color.Red().Printfln(translate.Get("commands.panel.writeSite.siteExist"))
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := os.Stat(path)
|
||||
if os.IsNotExist(err) {
|
||||
color.Red().Printfln(translate.Get("commands.panel.writeSite.pathNotExist"))
|
||||
return nil
|
||||
}
|
||||
|
||||
err = facades.Orm().Query().Create(&models.Website{
|
||||
Name: name,
|
||||
Status: status,
|
||||
Path: path,
|
||||
Php: php,
|
||||
Ssl: ssl,
|
||||
})
|
||||
if err != nil {
|
||||
color.Red().Printfln(translate.Get("commands.panel.writeSite.fail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Green().Printfln(translate.Get("commands.panel.writeSite.success"))
|
||||
|
||||
case "deleteSite":
|
||||
name := arg1
|
||||
if len(name) == 0 {
|
||||
color.Red().Printfln(translate.Get("commands.panel.deleteSite.paramFail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := facades.Orm().Query().Where("name", name).Delete(&models.Website{})
|
||||
if err != nil {
|
||||
color.Red().Printfln(translate.Get("commands.panel.deleteSite.fail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Green().Printfln(translate.Get("commands.panel.deleteSite.success"))
|
||||
|
||||
case "writeSetting":
|
||||
key := arg1
|
||||
value := arg2
|
||||
if len(key) == 0 || len(value) == 0 {
|
||||
color.Red().Printfln(translate.Get("commands.panel.writeSetting.paramFail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
var setting models.Setting
|
||||
err := facades.Orm().Query().UpdateOrCreate(&setting, models.Setting{
|
||||
Key: key,
|
||||
}, models.Setting{
|
||||
Value: value,
|
||||
})
|
||||
if err != nil {
|
||||
color.Red().Printfln(translate.Get("commands.panel.writeSetting.fail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Green().Printfln(translate.Get("commands.panel.writeSetting.success"))
|
||||
|
||||
case "getSetting":
|
||||
key := arg1
|
||||
if len(key) == 0 {
|
||||
color.Red().Printfln(translate.Get("commands.panel.getSetting.paramFail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
var setting models.Setting
|
||||
if err := facades.Orm().Query().Where("key", key).FirstOrFail(&setting); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("%s", setting.Value)
|
||||
|
||||
case "deleteSetting":
|
||||
key := arg1
|
||||
if len(key) == 0 {
|
||||
color.Red().Printfln(translate.Get("commands.panel.deleteSetting.paramFail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := facades.Orm().Query().Where("key", key).Delete(&models.Setting{})
|
||||
if err != nil {
|
||||
color.Red().Printfln(translate.Get("commands.panel.deleteSetting.fail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Green().Printfln(translate.Get("commands.panel.deleteSetting.success"))
|
||||
|
||||
case "addSite":
|
||||
name := arg1
|
||||
domain := arg2
|
||||
port := arg3
|
||||
path := arg4
|
||||
php := arg5
|
||||
if len(name) == 0 || len(domain) == 0 || len(port) == 0 || len(path) == 0 {
|
||||
color.Red().Printfln(translate.Get("commands.panel.addSite.paramFail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
domains := strings.Split(domain, ",")
|
||||
ports := strings.Split(port, ",")
|
||||
if len(domains) == 0 || len(ports) == 0 {
|
||||
color.Red().Printfln(translate.Get("commands.panel.addSite.paramFail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
var uintPorts []uint
|
||||
for _, p := range ports {
|
||||
uintPorts = append(uintPorts, cast.ToUint(p))
|
||||
}
|
||||
|
||||
website := services.NewWebsiteImpl()
|
||||
id, err := website.GetIDByName(name)
|
||||
if err != nil {
|
||||
color.Red().Printfln(err.Error())
|
||||
return nil
|
||||
}
|
||||
if id != 0 {
|
||||
color.Red().Printfln(translate.Get("commands.panel.addSite.siteExist"))
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = website.Add(types.WebsiteAdd{
|
||||
Name: name,
|
||||
Status: true,
|
||||
Domains: domains,
|
||||
Ports: uintPorts,
|
||||
Path: path,
|
||||
Php: php,
|
||||
Ssl: false,
|
||||
Db: false,
|
||||
})
|
||||
if err != nil {
|
||||
color.Red().Printfln(err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Green().Printfln(translate.Get("commands.panel.addSite.success"))
|
||||
|
||||
case "removeSite":
|
||||
name := arg1
|
||||
if len(name) == 0 {
|
||||
color.Red().Printfln(translate.Get("commands.panel.removeSite.paramFail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
website := services.NewWebsiteImpl()
|
||||
id, err := website.GetIDByName(name)
|
||||
if err != nil {
|
||||
color.Red().Printfln(err.Error())
|
||||
return nil
|
||||
}
|
||||
if id == 0 {
|
||||
color.Red().Printfln(translate.Get("commands.panel.removeSite.siteNotExist"))
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = website.Delete(id); err != nil {
|
||||
color.Red().Printfln(err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Green().Printfln(translate.Get("commands.panel.removeSite.success"))
|
||||
|
||||
case "installPlugin":
|
||||
slug := arg1
|
||||
if len(slug) == 0 {
|
||||
color.Red().Printfln(translate.Get("commands.panel.installPlugin.paramFail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
plugin := services.NewPluginImpl()
|
||||
if err := plugin.Install(slug); err != nil {
|
||||
color.Red().Printfln(err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Green().Printfln(translate.Get("commands.panel.installPlugin.success"))
|
||||
|
||||
case "uninstallPlugin":
|
||||
slug := arg1
|
||||
if len(slug) == 0 {
|
||||
color.Red().Printfln(translate.Get("commands.panel.uninstallPlugin.paramFail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
plugin := services.NewPluginImpl()
|
||||
if err := plugin.Uninstall(slug); err != nil {
|
||||
color.Red().Printfln(err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Green().Printfln(translate.Get("commands.panel.uninstallPlugin.success"))
|
||||
|
||||
case "updatePlugin":
|
||||
slug := arg1
|
||||
if len(slug) == 0 {
|
||||
color.Red().Printfln(translate.Get("commands.panel.updatePlugin.paramFail"))
|
||||
return nil
|
||||
}
|
||||
|
||||
plugin := services.NewPluginImpl()
|
||||
if err := plugin.Update(slug); err != nil {
|
||||
color.Red().Printfln(err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Green().Printfln(translate.Get("commands.panel.updatePlugin.success"))
|
||||
|
||||
default:
|
||||
color.Yellow().Printfln(facades.Config().GetString("panel.name") + " - " + translate.Get("commands.panel.tool") + " - " + facades.Config().GetString("panel.version"))
|
||||
color.Green().Printfln(translate.Get("commands.panel.use") + ":")
|
||||
color.Green().Printfln("panel update " + translate.Get("commands.panel.update.description"))
|
||||
color.Green().Printfln("panel getInfo " + translate.Get("commands.panel.getInfo.description"))
|
||||
color.Green().Printfln("panel getPort " + translate.Get("commands.panel.getPort.description"))
|
||||
color.Green().Printfln("panel getEntrance " + translate.Get("commands.panel.getEntrance.description"))
|
||||
color.Green().Printfln("panel deleteEntrance " + translate.Get("commands.panel.deleteEntrance.description"))
|
||||
color.Green().Printfln("panel cleanTask " + translate.Get("commands.panel.cleanTask.description"))
|
||||
color.Green().Printfln("panel backup {website/mysql/postgresql} {name} {path} {save_copies} " + translate.Get("commands.panel.backup.description"))
|
||||
color.Green().Printfln("panel cutoff {website_name} {save_copies} " + translate.Get("commands.panel.cutoff.description"))
|
||||
color.Green().Printfln("panel installPlugin {slug} " + translate.Get("commands.panel.installPlugin.description"))
|
||||
color.Green().Printfln("panel uninstallPlugin {slug} " + translate.Get("commands.panel.uninstallPlugin.description"))
|
||||
color.Green().Printfln("panel updatePlugin {slug} " + translate.Get("commands.panel.updatePlugin.description"))
|
||||
color.Green().Printfln("panel addSite {name} {domain} {port} {path} {php} " + translate.Get("commands.panel.addSite.description"))
|
||||
color.Green().Printfln("panel removeSite {name} " + translate.Get("commands.panel.removeSite.description"))
|
||||
color.Red().Printfln(translate.Get("commands.panel.forDeveloper") + ":")
|
||||
color.Yellow().Printfln("panel init " + translate.Get("commands.panel.init.description"))
|
||||
color.Yellow().Printfln("panel writePlugin {slug} {version} " + translate.Get("commands.panel.writePlugin.description"))
|
||||
color.Yellow().Printfln("panel deletePlugin {slug} " + translate.Get("commands.panel.deletePlugin.description"))
|
||||
color.Yellow().Printfln("panel writeMysqlPassword {password} " + translate.Get("commands.panel.writeMysqlPassword.description"))
|
||||
color.Yellow().Printfln("panel writeSite {name} {status} {path} {php} {ssl} " + translate.Get("commands.panel.writeSite.description"))
|
||||
color.Yellow().Printfln("panel deleteSite {name} " + translate.Get("commands.panel.deleteSite.description"))
|
||||
color.Yellow().Printfln("panel getSetting {name} " + translate.Get("commands.panel.getSetting.description"))
|
||||
color.Yellow().Printfln("panel writeSetting {name} {value} " + translate.Get("commands.panel.writeSetting.description"))
|
||||
color.Yellow().Printfln("panel deleteSetting {name} " + translate.Get("commands.panel.deleteSetting.description"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/goravel/framework/contracts/console"
|
||||
"github.com/goravel/framework/contracts/console/command"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
// PanelTask 面板每日任务
|
||||
type PanelTask struct {
|
||||
}
|
||||
|
||||
// Signature The name and signature of the console command.
|
||||
func (receiver *PanelTask) Signature() string {
|
||||
return "panel:task"
|
||||
}
|
||||
|
||||
// Description The console command description.
|
||||
func (receiver *PanelTask) Description() string {
|
||||
return facades.Lang(context.Background()).Get("commands.panel:task.description")
|
||||
}
|
||||
|
||||
// Extend The console command extend.
|
||||
func (receiver *PanelTask) Extend() command.Extend {
|
||||
return command.Extend{
|
||||
Category: "panel",
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Execute the console command.
|
||||
func (receiver *PanelTask) Handle(console.Context) error {
|
||||
types.Status = types.StatusMaintain
|
||||
|
||||
// 优化数据库
|
||||
if _, err := facades.Orm().Query().Exec("VACUUM"); err != nil {
|
||||
types.Status = types.StatusFailed
|
||||
facades.Log().Tags("面板", "每日任务").
|
||||
With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Error("优化面板数据库失败")
|
||||
return err
|
||||
}
|
||||
|
||||
// 备份面板
|
||||
if err := io.Archive([]string{"/www/panel"}, "/www/backup/panel/panel-"+carbon.Now().ToShortDateTimeString()+".zip"); err != nil {
|
||||
types.Status = types.StatusFailed
|
||||
facades.Log().Tags("面板", "每日任务").
|
||||
With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Error("备份面板失败")
|
||||
return err
|
||||
}
|
||||
|
||||
// 清理 7 天前的备份
|
||||
if _, err := shell.Execf(`find /www/backup/panel -mtime +7 -name "*.zip" -exec rm -rf {} \;`); err != nil {
|
||||
types.Status = types.StatusFailed
|
||||
facades.Log().Tags("面板", "每日任务").
|
||||
With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Error("清理面板备份失败")
|
||||
return err
|
||||
}
|
||||
|
||||
// 回收内存
|
||||
runtime.GC()
|
||||
debug.FreeOSMemory()
|
||||
|
||||
types.Status = types.StatusNormal
|
||||
return nil
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package console
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/console"
|
||||
"github.com/goravel/framework/contracts/schedule"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"github.com/TheTNB/panel/app/console/commands"
|
||||
)
|
||||
|
||||
type Kernel struct {
|
||||
}
|
||||
|
||||
func (kernel *Kernel) Schedule() []schedule.Event {
|
||||
return []schedule.Event{
|
||||
facades.Schedule().Command("panel:monitoring").EveryMinute().SkipIfStillRunning(),
|
||||
facades.Schedule().Command("panel:cert-renew").DailyAt("04:00").SkipIfStillRunning(),
|
||||
facades.Schedule().Command("panel:task").DailyAt("03:30").SkipIfStillRunning(),
|
||||
}
|
||||
}
|
||||
|
||||
func (kernel *Kernel) Commands() []console.Command {
|
||||
return []console.Command{
|
||||
&commands.Panel{},
|
||||
&commands.Monitoring{},
|
||||
&commands.CertRenew{},
|
||||
&commands.PanelTask{},
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"github.com/TheTNB/panel/internal"
|
||||
"github.com/TheTNB/panel/internal/services"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
)
|
||||
|
||||
type AssetController struct {
|
||||
setting internal.Setting
|
||||
}
|
||||
|
||||
func NewAssetController() *AssetController {
|
||||
return &AssetController{
|
||||
setting: services.NewSettingImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *AssetController) Index(ctx http.Context) http.Response {
|
||||
entrance := facades.Config().GetString("http.entrance")
|
||||
if entrance == "/" {
|
||||
entrance = ""
|
||||
}
|
||||
|
||||
// 自动纠正 URL 格式
|
||||
if ctx.Request().Path() == entrance && ctx.Request().Path() != "/" {
|
||||
return ctx.Response().Redirect(http.StatusMovedPermanently, ctx.Request().Path()+"/")
|
||||
}
|
||||
// 拒绝访问非入口文件
|
||||
if !strings.HasPrefix(ctx.Request().Path(), entrance) {
|
||||
return Error(ctx, http.StatusNotFound, http.StatusText(http.StatusNotFound))
|
||||
}
|
||||
|
||||
path := strings.TrimPrefix(ctx.Request().Path(), entrance)
|
||||
// 设置默认首页
|
||||
if path == "/" || path == "" {
|
||||
path = "/index.html"
|
||||
}
|
||||
|
||||
if !io.Exists("public" + path) {
|
||||
return Error(ctx, http.StatusNotFound, http.StatusText(http.StatusNotFound))
|
||||
}
|
||||
|
||||
file, err := os.Open("public" + path)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
stat, err := file.Stat()
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
if stat.IsDir() {
|
||||
return Error(ctx, http.StatusForbidden, http.StatusText(http.StatusForbidden))
|
||||
}
|
||||
|
||||
return ctx.Response().Header("Cache-Control", "no-cache").File("public" + path)
|
||||
}
|
||||
|
||||
func (r *AssetController) Favicon(ctx http.Context) http.Response {
|
||||
return ctx.Response().File("public/favicon.png")
|
||||
}
|
||||
|
||||
func (r *AssetController) Robots(ctx http.Context) http.Response {
|
||||
return ctx.Response().File("public/robots.txt")
|
||||
}
|
||||
|
||||
func (r *AssetController) NotFound(ctx http.Context) http.Response {
|
||||
return Error(ctx, http.StatusNotFound, http.StatusText(http.StatusNotFound))
|
||||
}
|
||||
@@ -1,701 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
requests "github.com/TheTNB/panel/app/http/requests/cert"
|
||||
commonrequests "github.com/TheTNB/panel/app/http/requests/common"
|
||||
"github.com/TheTNB/panel/app/models"
|
||||
"github.com/TheTNB/panel/internal"
|
||||
"github.com/TheTNB/panel/internal/services"
|
||||
"github.com/TheTNB/panel/pkg/acme"
|
||||
)
|
||||
|
||||
type CertController struct {
|
||||
cron internal.Cron
|
||||
cert internal.Cert
|
||||
}
|
||||
|
||||
func NewCertController() *CertController {
|
||||
return &CertController{
|
||||
cron: services.NewCronImpl(),
|
||||
cert: services.NewCertImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// CAProviders
|
||||
//
|
||||
// @Summary 获取 CA 提供商
|
||||
// @Description 获取面板证书管理支持的 CA 提供商
|
||||
// @Tags TLS证书
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/cert/caProviders [get]
|
||||
func (r *CertController) CAProviders(ctx http.Context) http.Response {
|
||||
return Success(ctx, []map[string]string{
|
||||
{
|
||||
"name": "Let's Encrypt",
|
||||
"ca": "letsencrypt",
|
||||
},
|
||||
{
|
||||
"name": "ZeroSSL",
|
||||
"ca": "zerossl",
|
||||
},
|
||||
{
|
||||
"name": "SSL.com",
|
||||
"ca": "sslcom",
|
||||
},
|
||||
{
|
||||
"name": "Google",
|
||||
"ca": "google",
|
||||
},
|
||||
{
|
||||
"name": "Buypass",
|
||||
"ca": "buypass",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// DNSProviders
|
||||
//
|
||||
// @Summary 获取 DNS 提供商
|
||||
// @Description 获取面板证书管理支持的 DNS 提供商
|
||||
// @Tags TLS证书
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/cert/dnsProviders [get]
|
||||
func (r *CertController) DNSProviders(ctx http.Context) http.Response {
|
||||
return Success(ctx, []map[string]any{
|
||||
{
|
||||
"name": "DNSPod",
|
||||
"dns": acme.DnsPod,
|
||||
},
|
||||
{
|
||||
"name": "阿里云",
|
||||
"dns": acme.AliYun,
|
||||
},
|
||||
{
|
||||
"name": "CloudFlare",
|
||||
"dns": acme.CloudFlare,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Algorithms
|
||||
//
|
||||
// @Summary 获取算法列表
|
||||
// @Description 获取面板证书管理支持的算法列表
|
||||
// @Tags TLS证书
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/cert/algorithms [get]
|
||||
func (r *CertController) Algorithms(ctx http.Context) http.Response {
|
||||
return Success(ctx, []map[string]any{
|
||||
{
|
||||
"name": "EC256",
|
||||
"key": acme.KeyEC256,
|
||||
},
|
||||
{
|
||||
"name": "EC384",
|
||||
"key": acme.KeyEC384,
|
||||
},
|
||||
{
|
||||
"name": "RSA2048",
|
||||
"key": acme.KeyRSA2048,
|
||||
},
|
||||
{
|
||||
"name": "RSA4096",
|
||||
"key": acme.KeyRSA4096,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// UserList
|
||||
//
|
||||
// @Summary 获取用户列表
|
||||
// @Description 获取面板证书管理的 ACME 用户列表
|
||||
// @Tags TLS证书
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data query commonrequests.Paginate true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/cert/users [get]
|
||||
func (r *CertController) UserList(ctx http.Context) http.Response {
|
||||
var paginateRequest commonrequests.Paginate
|
||||
sanitize := SanitizeRequest(ctx, &paginateRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
var users []models.CertUser
|
||||
var total int64
|
||||
err := facades.Orm().Query().Paginate(paginateRequest.Page, paginateRequest.Limit, &users, &total)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("获取ACME用户列表失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"items": users,
|
||||
})
|
||||
}
|
||||
|
||||
// UserStore
|
||||
//
|
||||
// @Summary 添加 ACME 用户
|
||||
// @Description 添加 ACME 用户到面板证书管理
|
||||
// @Tags TLS证书
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.UserStore true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/cert/users [post]
|
||||
func (r *CertController) UserStore(ctx http.Context) http.Response {
|
||||
var storeRequest requests.UserStore
|
||||
sanitize := SanitizeRequest(ctx, &storeRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
err := r.cert.UserStore(storeRequest)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("添加ACME用户失败")
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// UserUpdate
|
||||
//
|
||||
// @Summary 更新 ACME 用户
|
||||
// @Description 更新面板证书管理的 ACME 用户
|
||||
// @Tags TLS证书
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param id path int true "用户 ID"
|
||||
// @Param data body requests.UserUpdate true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/cert/users/{id} [put]
|
||||
func (r *CertController) UserUpdate(ctx http.Context) http.Response {
|
||||
var updateRequest requests.UserUpdate
|
||||
sanitize := SanitizeRequest(ctx, &updateRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
err := r.cert.UserUpdate(updateRequest)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{
|
||||
"userID": updateRequest.ID,
|
||||
"error": err.Error(),
|
||||
}).Info("更新ACME用户失败")
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// UserShow
|
||||
//
|
||||
// @Summary 获取 ACME 用户
|
||||
// @Description 获取面板证书管理的 ACME 用户
|
||||
// @Tags TLS证书
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param id path int true "用户 ID"
|
||||
// @Success 200 {object} SuccessResponse{data=models.CertUser}
|
||||
// @Router /panel/cert/users/{id} [get]
|
||||
func (r *CertController) UserShow(ctx http.Context) http.Response {
|
||||
var showAndDestroyRequest requests.UserShowAndDestroy
|
||||
sanitize := SanitizeRequest(ctx, &showAndDestroyRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
user, err := r.cert.UserShow(showAndDestroyRequest.ID)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{
|
||||
"userID": showAndDestroyRequest.ID,
|
||||
"error": err.Error(),
|
||||
}).Info("获取ACME用户失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, user)
|
||||
}
|
||||
|
||||
// UserDestroy
|
||||
//
|
||||
// @Summary 删除 ACME 用户
|
||||
// @Description 删除面板证书管理的 ACME 用户
|
||||
// @Tags TLS证书
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param id path int true "用户 ID"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/cert/users/{id} [delete]
|
||||
func (r *CertController) UserDestroy(ctx http.Context) http.Response {
|
||||
var showAndDestroyRequest requests.UserShowAndDestroy
|
||||
sanitize := SanitizeRequest(ctx, &showAndDestroyRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
err := r.cert.UserDestroy(showAndDestroyRequest.ID)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{
|
||||
"userID": showAndDestroyRequest.ID,
|
||||
"error": err.Error(),
|
||||
}).Info("删除ACME用户失败")
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// DNSList
|
||||
//
|
||||
// @Summary 获取 DNS 接口列表
|
||||
// @Description 获取面板证书管理的 DNS 接口列表
|
||||
// @Tags TLS证书
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data query commonrequests.Paginate true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/cert/dns [get]
|
||||
func (r *CertController) DNSList(ctx http.Context) http.Response {
|
||||
var paginateRequest commonrequests.Paginate
|
||||
sanitize := SanitizeRequest(ctx, &paginateRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
var dns []models.CertDNS
|
||||
var total int64
|
||||
err := facades.Orm().Query().Paginate(paginateRequest.Page, paginateRequest.Limit, &dns, &total)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("获取DNS接口列表失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"items": dns,
|
||||
})
|
||||
}
|
||||
|
||||
// DNSStore
|
||||
//
|
||||
// @Summary 添加 DNS 接口
|
||||
// @Description 添加 DNS 接口到面板证书管理
|
||||
// @Tags TLS证书
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.DNSStore true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/cert/dns [post]
|
||||
func (r *CertController) DNSStore(ctx http.Context) http.Response {
|
||||
var storeRequest requests.DNSStore
|
||||
sanitize := SanitizeRequest(ctx, &storeRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
err := r.cert.DNSStore(storeRequest)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("添加DNS接口失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// DNSShow
|
||||
//
|
||||
// @Summary 获取 DNS 接口
|
||||
// @Description 获取面板证书管理的 DNS 接口
|
||||
// @Tags TLS证书
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param id path int true "DNS 接口 ID"
|
||||
// @Success 200 {object} SuccessResponse{data=models.CertDNS}
|
||||
// @Router /panel/cert/dns/{id} [get]
|
||||
func (r *CertController) DNSShow(ctx http.Context) http.Response {
|
||||
var showAndDestroyRequest requests.DNSShowAndDestroy
|
||||
sanitize := SanitizeRequest(ctx, &showAndDestroyRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
dns, err := r.cert.DNSShow(showAndDestroyRequest.ID)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{
|
||||
"dnsID": showAndDestroyRequest.ID,
|
||||
"error": err.Error(),
|
||||
}).Info("获取DNS接口失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, dns)
|
||||
}
|
||||
|
||||
// DNSUpdate
|
||||
//
|
||||
// @Summary 更新 DNS 接口
|
||||
// @Description 更新面板证书管理的 DNS 接口
|
||||
// @Tags TLS证书
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param id path int true "DNS 接口 ID"
|
||||
// @Param data body requests.DNSUpdate true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/cert/dns/{id} [put]
|
||||
func (r *CertController) DNSUpdate(ctx http.Context) http.Response {
|
||||
var updateRequest requests.DNSUpdate
|
||||
sanitize := SanitizeRequest(ctx, &updateRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
err := r.cert.DNSUpdate(updateRequest)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{
|
||||
"dnsID": updateRequest.ID,
|
||||
"error": err.Error(),
|
||||
}).Info("更新DNS接口失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// DNSDestroy
|
||||
//
|
||||
// @Summary 删除 DNS 接口
|
||||
// @Description 删除面板证书管理的 DNS 接口
|
||||
// @Tags TLS证书
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param id path int true "DNS 接口 ID"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/cert/dns/{id} [delete]
|
||||
func (r *CertController) DNSDestroy(ctx http.Context) http.Response {
|
||||
var showAndDestroyRequest requests.DNSShowAndDestroy
|
||||
sanitize := SanitizeRequest(ctx, &showAndDestroyRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
err := r.cert.DNSDestroy(showAndDestroyRequest.ID)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{
|
||||
"dnsID": showAndDestroyRequest.ID,
|
||||
"error": err.Error(),
|
||||
}).Info("删除DNS接口失败")
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// CertList
|
||||
//
|
||||
// @Summary 获取证书列表
|
||||
// @Description 获取面板证书管理的证书列表
|
||||
// @Tags TLS证书
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data query commonrequests.Paginate true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/cert/certs [get]
|
||||
func (r *CertController) CertList(ctx http.Context) http.Response {
|
||||
var paginateRequest commonrequests.Paginate
|
||||
sanitize := SanitizeRequest(ctx, &paginateRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
var certs []models.Cert
|
||||
var total int64
|
||||
err := facades.Orm().Query().With("Website").With("User").With("DNS").Paginate(paginateRequest.Page, paginateRequest.Limit, &certs, &total)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("获取证书列表失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"items": certs,
|
||||
})
|
||||
}
|
||||
|
||||
// CertStore
|
||||
//
|
||||
// @Summary 添加证书
|
||||
// @Description 添加证书到面板证书管理
|
||||
// @Tags TLS证书
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.CertStore true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/cert/certs [post]
|
||||
func (r *CertController) CertStore(ctx http.Context) http.Response {
|
||||
var storeRequest requests.CertStore
|
||||
sanitize := SanitizeRequest(ctx, &storeRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
err := r.cert.CertStore(storeRequest)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("添加证书失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// CertUpdate
|
||||
//
|
||||
// @Summary 更新证书
|
||||
// @Description 更新面板证书管理的证书
|
||||
// @Tags TLS证书
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param id path int true "证书 ID"
|
||||
// @Param data body requests.CertUpdate true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/cert/certs/{id} [put]
|
||||
func (r *CertController) CertUpdate(ctx http.Context) http.Response {
|
||||
var updateRequest requests.CertUpdate
|
||||
sanitize := SanitizeRequest(ctx, &updateRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
err := r.cert.CertUpdate(updateRequest)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{
|
||||
"certID": updateRequest.ID,
|
||||
"error": err.Error(),
|
||||
}).Info("更新证书失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// CertShow
|
||||
//
|
||||
// @Summary 获取证书
|
||||
// @Description 获取面板证书管理的证书
|
||||
// @Tags TLS证书
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param id path int true "证书 ID"
|
||||
// @Success 200 {object} SuccessResponse{data=models.Cert}
|
||||
// @Router /panel/cert/certs/{id} [get]
|
||||
func (r *CertController) CertShow(ctx http.Context) http.Response {
|
||||
var showAndDestroyRequest requests.CertShowAndDestroy
|
||||
sanitize := SanitizeRequest(ctx, &showAndDestroyRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
cert, err := r.cert.CertShow(showAndDestroyRequest.ID)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{
|
||||
"certID": showAndDestroyRequest.ID,
|
||||
"error": err.Error(),
|
||||
}).Info("获取证书失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, cert)
|
||||
}
|
||||
|
||||
// CertDestroy
|
||||
//
|
||||
// @Summary 删除证书
|
||||
// @Description 删除面板证书管理的证书
|
||||
// @Tags TLS证书
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param id path int true "证书 ID"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/cert/certs/{id} [delete]
|
||||
func (r *CertController) CertDestroy(ctx http.Context) http.Response {
|
||||
var showAndDestroyRequest requests.CertShowAndDestroy
|
||||
sanitize := SanitizeRequest(ctx, &showAndDestroyRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
err := r.cert.CertDestroy(showAndDestroyRequest.ID)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{
|
||||
"certID": showAndDestroyRequest.ID,
|
||||
"error": err.Error(),
|
||||
}).Info("删除证书失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Obtain
|
||||
//
|
||||
// @Summary 签发证书
|
||||
// @Description 签发面板证书管理的证书
|
||||
// @Tags TLS证书
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.Obtain true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/cert/obtain [post]
|
||||
func (r *CertController) Obtain(ctx http.Context) http.Response {
|
||||
var obtainRequest requests.Obtain
|
||||
sanitize := SanitizeRequest(ctx, &obtainRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
cert, err := r.cert.CertShow(obtainRequest.ID)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{
|
||||
"certID": obtainRequest.ID,
|
||||
"error": err.Error(),
|
||||
}).Info("获取证书失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
if cert.DNS != nil || cert.Website != nil {
|
||||
_, err = r.cert.ObtainAuto(obtainRequest.ID)
|
||||
} else {
|
||||
_, err = r.cert.ObtainManual(obtainRequest.ID)
|
||||
}
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("签发证书失败")
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Renew
|
||||
//
|
||||
// @Summary 续签证书
|
||||
// @Description 续签面板证书管理的证书
|
||||
// @Tags TLS证书
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.Renew true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/cert/renew [post]
|
||||
func (r *CertController) Renew(ctx http.Context) http.Response {
|
||||
var renewRequest requests.Renew
|
||||
sanitize := SanitizeRequest(ctx, &renewRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
_, err := r.cert.Renew(renewRequest.ID)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("续签证书失败")
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// ManualDNS
|
||||
//
|
||||
// @Summary 获取手动 DNS 记录
|
||||
// @Description 获取签发证书所需的 DNS 记录
|
||||
// @Tags TLS证书
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.Obtain true "request"
|
||||
// @Success 200 {object} SuccessResponse{data=[]acme.DNSRecord}
|
||||
// @Router /panel/cert/manualDNS [post]
|
||||
func (r *CertController) ManualDNS(ctx http.Context) http.Response {
|
||||
var obtainRequest requests.Obtain
|
||||
sanitize := SanitizeRequest(ctx, &obtainRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
resolves, err := r.cert.ManualDNS(obtainRequest.ID)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("获取手动DNS记录失败")
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, resolves)
|
||||
}
|
||||
|
||||
// Deploy
|
||||
//
|
||||
// @Summary 部署证书
|
||||
// @Description 部署面板证书管理的证书
|
||||
// @Tags TLS证书
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.CertDeploy true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/cert/deploy [post]
|
||||
func (r *CertController) Deploy(ctx http.Context) http.Response {
|
||||
var deployRequest requests.CertDeploy
|
||||
sanitize := SanitizeRequest(ctx, &deployRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
err := r.cert.Deploy(deployRequest.ID, deployRequest.WebsiteID)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{
|
||||
"certID": deployRequest.ID,
|
||||
"error": err.Error(),
|
||||
}).Info("部署证书失败")
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
commonrequests "github.com/TheTNB/panel/app/http/requests/common"
|
||||
)
|
||||
|
||||
// SuccessResponse 通用成功响应
|
||||
type SuccessResponse struct {
|
||||
Message string `json:"message"`
|
||||
Data any `json:"data"`
|
||||
}
|
||||
|
||||
// ErrorResponse 通用错误响应
|
||||
type ErrorResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Success 响应成功
|
||||
func Success(ctx http.Context, data any) http.Response {
|
||||
return ctx.Response().Success().Json(&SuccessResponse{
|
||||
Message: "success",
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// Error 响应错误
|
||||
func Error(ctx http.Context, code int, message string) http.Response {
|
||||
return ctx.Response().Json(code, &ErrorResponse{
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// ErrorSystem 响应系统错误
|
||||
func ErrorSystem(ctx http.Context) http.Response {
|
||||
return ctx.Response().Json(http.StatusInternalServerError, &ErrorResponse{
|
||||
Message: facades.Lang(ctx).Get("errors.internal"),
|
||||
})
|
||||
}
|
||||
|
||||
func Paginate[T any](ctx http.Context, allItems []T) (pagedItems []T, total int) {
|
||||
var paginateRequest commonrequests.Paginate
|
||||
sanitize := SanitizeRequest(ctx, &paginateRequest)
|
||||
if sanitize != nil {
|
||||
return []T{}, 0
|
||||
}
|
||||
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
total = len(allItems)
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
|
||||
if total == 0 {
|
||||
return []T{}, 0
|
||||
}
|
||||
if startIndex > total {
|
||||
return []T{}, total
|
||||
}
|
||||
if endIndex > total {
|
||||
endIndex = total
|
||||
}
|
||||
|
||||
return allItems[startIndex:endIndex], total
|
||||
}
|
||||
|
||||
// SanitizeRequest 消毒请求参数
|
||||
func SanitizeRequest(ctx http.Context, request http.FormRequest) http.Response {
|
||||
errors, err := ctx.Request().ValidateRequest(request)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusUnprocessableEntity, err.Error())
|
||||
}
|
||||
if errors != nil {
|
||||
return Error(ctx, http.StatusUnprocessableEntity, errors.One())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sanitize 消毒参数
|
||||
func Sanitize(ctx http.Context, rules map[string]string) http.Response {
|
||||
validator, err := ctx.Request().Validate(rules)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusUnprocessableEntity, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,295 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/TheTNB/panel/app/models"
|
||||
"github.com/TheTNB/panel/internal"
|
||||
"github.com/TheTNB/panel/internal/services"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
)
|
||||
|
||||
type CronController struct {
|
||||
cron internal.Cron
|
||||
setting internal.Setting
|
||||
}
|
||||
|
||||
func NewCronController() *CronController {
|
||||
return &CronController{
|
||||
cron: services.NewCronImpl(),
|
||||
setting: services.NewSettingImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// List 获取计划任务列表
|
||||
func (r *CronController) List(ctx http.Context) http.Response {
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
|
||||
var crons []models.Cron
|
||||
var total int64
|
||||
err := facades.Orm().Query().Paginate(page, limit, &crons, &total)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "计划任务").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("查询计划任务列表失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"items": crons,
|
||||
})
|
||||
}
|
||||
|
||||
// Add 添加计划任务
|
||||
func (r *CronController) Add(ctx http.Context) http.Response {
|
||||
if sanitize := Sanitize(ctx, map[string]string{
|
||||
"name": "required|min_len:1|max_len:255",
|
||||
"time": "required",
|
||||
"script": "required",
|
||||
"type": "required|in:shell,backup,cutoff",
|
||||
"backup_type": "required_if:type,backup|in:website,mysql,postgresql",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
// 单独验证时间格式
|
||||
if !regexp.MustCompile(`^((\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+)(,(\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+))*\s?){5}$`).MatchString(ctx.Request().Input("time")) {
|
||||
return Error(ctx, http.StatusUnprocessableEntity, "时间格式错误")
|
||||
}
|
||||
|
||||
script := ctx.Request().Input("script")
|
||||
cronType := ctx.Request().Input("type")
|
||||
if cronType == "backup" {
|
||||
backupType := ctx.Request().Input("backup_type")
|
||||
backupName := ctx.Request().Input("database")
|
||||
if backupType == "website" {
|
||||
backupName = ctx.Request().Input("website")
|
||||
}
|
||||
backupPath := ctx.Request().Input("backup_path")
|
||||
if len(backupPath) == 0 {
|
||||
backupPath = r.setting.Get(models.SettingKeyBackupPath) + "/" + backupType
|
||||
}
|
||||
backupSave := ctx.Request().InputInt("save", 10)
|
||||
script = `#!/bin/bash
|
||||
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
|
||||
|
||||
# 耗子面板 - 数据备份脚本
|
||||
|
||||
type=` + backupType + `
|
||||
path=` + backupPath + `
|
||||
name=` + backupName + `
|
||||
save=` + cast.ToString(backupSave) + `
|
||||
|
||||
# 执行备份
|
||||
panel backup ${type} ${name} ${path} ${save} 2>&1
|
||||
`
|
||||
}
|
||||
if cronType == "cutoff" {
|
||||
website := ctx.Request().Input("website")
|
||||
save := ctx.Request().InputInt("save", 180)
|
||||
script = `#!/bin/bash
|
||||
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
|
||||
|
||||
# 耗子面板 - 日志切割脚本
|
||||
|
||||
name=` + website + `
|
||||
save=` + cast.ToString(save) + `
|
||||
|
||||
# 执行切割
|
||||
panel cutoff ${name} ${save} 2>&1
|
||||
`
|
||||
}
|
||||
|
||||
shellDir := "/www/server/cron/"
|
||||
shellLogDir := "/www/server/cron/logs/"
|
||||
if !io.Exists(shellDir) {
|
||||
return Error(ctx, http.StatusInternalServerError, "计划任务目录不存在")
|
||||
}
|
||||
if !io.Exists(shellLogDir) {
|
||||
return Error(ctx, http.StatusInternalServerError, "计划任务日志目录不存在")
|
||||
}
|
||||
shellFile := strconv.Itoa(int(carbon.Now().Timestamp())) + str.RandomString(16)
|
||||
if err := io.Write(shellDir+shellFile+".sh", script, 0700); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if out, err := shell.Execf("dos2unix " + shellDir + shellFile + ".sh"); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
var cron models.Cron
|
||||
cron.Name = ctx.Request().Input("name")
|
||||
cron.Type = ctx.Request().Input("type")
|
||||
cron.Status = true
|
||||
cron.Time = ctx.Request().Input("time")
|
||||
cron.Shell = shellDir + shellFile + ".sh"
|
||||
cron.Log = shellLogDir + shellFile + ".log"
|
||||
|
||||
if err := facades.Orm().Query().Create(&cron); err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "计划任务").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("保存计划任务失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
if err := r.cron.AddToSystem(cron); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"id": cron.ID,
|
||||
})
|
||||
}
|
||||
|
||||
// Script 获取脚本内容
|
||||
func (r *CronController) Script(ctx http.Context) http.Response {
|
||||
var cron models.Cron
|
||||
err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusUnprocessableEntity, "计划任务不存在")
|
||||
}
|
||||
|
||||
script, err := io.Read(cron.Shell)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, script)
|
||||
}
|
||||
|
||||
// Update 更新计划任务
|
||||
func (r *CronController) Update(ctx http.Context) http.Response {
|
||||
if sanitize := Sanitize(ctx, map[string]string{
|
||||
"name": "required|min_len:1|max_len:255",
|
||||
"time": "required",
|
||||
"script": "required",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
// 单独验证时间格式
|
||||
if !regexp.MustCompile(`^((\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+)(,(\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+))*\s?){5}$`).MatchString(ctx.Request().Input("time")) {
|
||||
return Error(ctx, http.StatusUnprocessableEntity, "时间格式错误")
|
||||
}
|
||||
|
||||
var cron models.Cron
|
||||
if err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron); err != nil {
|
||||
return Error(ctx, http.StatusUnprocessableEntity, "计划任务不存在")
|
||||
}
|
||||
|
||||
if !cron.Status {
|
||||
return Error(ctx, http.StatusUnprocessableEntity, "计划任务已禁用")
|
||||
}
|
||||
|
||||
cron.Time = ctx.Request().Input("time")
|
||||
cron.Name = ctx.Request().Input("name")
|
||||
if err := facades.Orm().Query().Save(&cron); err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "计划任务").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("更新计划任务失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
if err := io.Write(cron.Shell, ctx.Request().Input("script"), 0644); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if out, err := shell.Execf("dos2unix " + cron.Shell); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
if err := r.cron.DeleteFromSystem(cron); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if cron.Status {
|
||||
if err := r.cron.AddToSystem(cron); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Delete 删除计划任务
|
||||
func (r *CronController) Delete(ctx http.Context) http.Response {
|
||||
var cron models.Cron
|
||||
if err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron); err != nil {
|
||||
return Error(ctx, http.StatusUnprocessableEntity, "计划任务不存在")
|
||||
}
|
||||
|
||||
if err := r.cron.DeleteFromSystem(cron); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if err := io.Remove(cron.Shell); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if _, err := facades.Orm().Query().Delete(&cron); err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "计划任务").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("删除计划任务失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Status 更新计划任务状态
|
||||
func (r *CronController) Status(ctx http.Context) http.Response {
|
||||
if sanitize := Sanitize(ctx, map[string]string{
|
||||
"status": "bool",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
var cron models.Cron
|
||||
if err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron); err != nil {
|
||||
return Error(ctx, http.StatusUnprocessableEntity, "计划任务不存在")
|
||||
}
|
||||
|
||||
cron.Status = ctx.Request().InputBool("status")
|
||||
if err := facades.Orm().Query().Save(&cron); err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "计划任务").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("更新计划任务状态失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
if err := r.cron.DeleteFromSystem(cron); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if cron.Status {
|
||||
if err := r.cron.AddToSystem(cron); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Log 获取计划任务日志
|
||||
func (r *CronController) Log(ctx http.Context) http.Response {
|
||||
var cron models.Cron
|
||||
if err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron); err != nil {
|
||||
return Error(ctx, http.StatusUnprocessableEntity, "计划任务不存在")
|
||||
}
|
||||
|
||||
if !io.Exists(cron.Log) {
|
||||
return Error(ctx, http.StatusUnprocessableEntity, "日志文件不存在")
|
||||
}
|
||||
|
||||
log, err := shell.Execf("tail -n 1000 " + cron.Log)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, log)
|
||||
}
|
||||
@@ -1,518 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
stdio "io"
|
||||
stdos "os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
|
||||
requests "github.com/TheTNB/panel/app/http/requests/file"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/os"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
)
|
||||
|
||||
type FileController struct {
|
||||
}
|
||||
|
||||
func NewFileController() *FileController {
|
||||
return &FileController{}
|
||||
}
|
||||
|
||||
// Create
|
||||
//
|
||||
// @Summary 创建文件/目录
|
||||
// @Description 创建文件/目录到给定路径
|
||||
// @Tags 文件
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.NotExist true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/file/create [post]
|
||||
func (r *FileController) Create(ctx http.Context) http.Response {
|
||||
var request requests.NotExist
|
||||
sanitize := SanitizeRequest(ctx, &request)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
isDir := ctx.Request().InputBool("dir")
|
||||
if !isDir {
|
||||
if out, err := shell.Execf("touch " + request.Path); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
} else {
|
||||
if err := io.Mkdir(request.Path, 0755); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
r.setPermission(request.Path, 0755, "www", "www")
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Content
|
||||
//
|
||||
// @Summary 获取文件内容
|
||||
// @Description 获取给定路径的文件内容
|
||||
// @Tags 文件
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data query requests.Exist true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/file/content [get]
|
||||
func (r *FileController) Content(ctx http.Context) http.Response {
|
||||
var request requests.Exist
|
||||
sanitize := SanitizeRequest(ctx, &request)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
fileInfo, err := io.FileInfo(request.Path)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if fileInfo.IsDir() {
|
||||
return Error(ctx, http.StatusInternalServerError, "目标路径不是文件")
|
||||
}
|
||||
if fileInfo.Size() > 10*1024*1024 {
|
||||
return Error(ctx, http.StatusInternalServerError, "文件大小超过 10 M,不支持在线编辑")
|
||||
}
|
||||
|
||||
content, err := io.Read(request.Path)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, content)
|
||||
}
|
||||
|
||||
// Save
|
||||
//
|
||||
// @Summary 保存文件内容
|
||||
// @Description 保存给定路径的文件内容
|
||||
// @Tags 文件
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.Save true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/file/save [post]
|
||||
func (r *FileController) Save(ctx http.Context) http.Response {
|
||||
var request requests.Save
|
||||
sanitize := SanitizeRequest(ctx, &request)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
fileInfo, err := io.FileInfo(request.Path)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if err = io.Write(request.Path, request.Content, fileInfo.Mode()); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
r.setPermission(request.Path, 0755, "www", "www")
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Delete
|
||||
//
|
||||
// @Summary 删除文件/目录
|
||||
// @Description 删除给定路径的文件/目录
|
||||
// @Tags 文件
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.Exist true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/file/delete [post]
|
||||
func (r *FileController) Delete(ctx http.Context) http.Response {
|
||||
var request requests.Exist
|
||||
sanitize := SanitizeRequest(ctx, &request)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
if err := io.Remove(request.Path); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Upload
|
||||
//
|
||||
// @Summary 上传文件
|
||||
// @Description 上传文件到给定路径
|
||||
// @Tags 文件
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param file formData file true "file"
|
||||
// @Param path formData string true "path"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/file/upload [post]
|
||||
func (r *FileController) Upload(ctx http.Context) http.Response {
|
||||
var request requests.Upload
|
||||
sanitize := SanitizeRequest(ctx, &request)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
src, err := request.File.Open()
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
if io.Exists(request.Path) && !ctx.Request().InputBool("force") {
|
||||
return Error(ctx, http.StatusForbidden, "目标路径已存在,是否覆盖?")
|
||||
}
|
||||
|
||||
data, err := stdio.ReadAll(src)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if err = io.Write(request.Path, string(data), 0755); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
r.setPermission(request.Path, 0755, "www", "www")
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Move
|
||||
//
|
||||
// @Summary 移动文件/目录
|
||||
// @Description 移动文件/目录到给定路径,等效于重命名
|
||||
// @Tags 文件
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.Move true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/file/move [post]
|
||||
func (r *FileController) Move(ctx http.Context) http.Response {
|
||||
var request requests.Move
|
||||
sanitize := SanitizeRequest(ctx, &request)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
if io.Exists(request.Target) && !ctx.Request().InputBool("force") {
|
||||
return Error(ctx, http.StatusForbidden, "目标路径"+request.Target+"已存在")
|
||||
}
|
||||
|
||||
if err := io.Mv(request.Source, request.Target); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
r.setPermission(request.Target, 0755, "www", "www")
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Copy
|
||||
//
|
||||
// @Summary 复制文件/目录
|
||||
// @Description 复制文件/目录到给定路径
|
||||
// @Tags 文件
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.Copy true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/file/copy [post]
|
||||
func (r *FileController) Copy(ctx http.Context) http.Response {
|
||||
var request requests.Copy
|
||||
sanitize := SanitizeRequest(ctx, &request)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
if io.Exists(request.Target) && !ctx.Request().InputBool("force") {
|
||||
return Error(ctx, http.StatusForbidden, "目标路径"+request.Target+"已存在")
|
||||
}
|
||||
|
||||
if err := io.Cp(request.Source, request.Target); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
r.setPermission(request.Source, 0755, "www", "www")
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Download
|
||||
//
|
||||
// @Summary 下载文件
|
||||
// @Description 下载给定路径的文件
|
||||
// @Tags 文件
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data query requests.NotExist true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/file/download [get]
|
||||
func (r *FileController) Download(ctx http.Context) http.Response {
|
||||
var request requests.Exist
|
||||
sanitize := SanitizeRequest(ctx, &request)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
info, err := io.FileInfo(request.Path)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if info.IsDir() {
|
||||
return Error(ctx, http.StatusInternalServerError, "不能下载目录")
|
||||
}
|
||||
|
||||
return ctx.Response().Download(request.Path, info.Name())
|
||||
}
|
||||
|
||||
// RemoteDownload
|
||||
//
|
||||
// @Summary 下载远程文件
|
||||
// @Description 下载远程文件到给定路径
|
||||
// @Tags 文件
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.NotExist true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/file/remoteDownload [post]
|
||||
func (r *FileController) RemoteDownload(ctx http.Context) http.Response {
|
||||
var request requests.NotExist
|
||||
sanitize := SanitizeRequest(ctx, &request)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
// TODO 使用异步任务下载文件
|
||||
return nil
|
||||
}
|
||||
|
||||
// Info
|
||||
//
|
||||
// @Summary 获取文件/目录信息
|
||||
// @Description 获取给定路径的文件/目录信息
|
||||
// @Tags 文件
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data query requests.Exist true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/file/info [get]
|
||||
func (r *FileController) Info(ctx http.Context) http.Response {
|
||||
var request requests.Exist
|
||||
sanitize := SanitizeRequest(ctx, &request)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
fileInfo, err := io.FileInfo(request.Path)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"name": fileInfo.Name(),
|
||||
"size": str.FormatBytes(float64(fileInfo.Size())),
|
||||
"mode_str": fileInfo.Mode().String(),
|
||||
"mode": fmt.Sprintf("%04o", fileInfo.Mode().Perm()),
|
||||
"dir": fileInfo.IsDir(),
|
||||
"modify": carbon.FromStdTime(fileInfo.ModTime()).ToDateTimeString(),
|
||||
})
|
||||
}
|
||||
|
||||
// Permission
|
||||
//
|
||||
// @Summary 修改文件/目录权限
|
||||
// @Description 修改给定路径的文件/目录权限
|
||||
// @Tags 文件
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.Permission true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/file/permission [post]
|
||||
func (r *FileController) Permission(ctx http.Context) http.Response {
|
||||
var request requests.Permission
|
||||
sanitize := SanitizeRequest(ctx, &request)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
// 解析成8进制
|
||||
mode, err := strconv.ParseUint(request.Mode, 8, 64)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if err = io.Chmod(request.Path, stdos.FileMode(mode)); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if err = io.Chown(request.Path, request.Owner, request.Group); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Archive
|
||||
//
|
||||
// @Summary 压缩文件/目录
|
||||
// @Description 压缩文件/目录到给定路径
|
||||
// @Tags 文件
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.Archive true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/file/archive [post]
|
||||
func (r *FileController) Archive(ctx http.Context) http.Response {
|
||||
var request requests.Archive
|
||||
sanitize := SanitizeRequest(ctx, &request)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
if err := io.Archive(request.Paths, request.File); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
r.setPermission(request.File, 0755, "www", "www")
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// UnArchive
|
||||
//
|
||||
// @Summary 解压文件/目录
|
||||
// @Description 解压文件/目录到给定路径
|
||||
// @Tags 文件
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.UnArchive true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/file/unArchive [post]
|
||||
func (r *FileController) UnArchive(ctx http.Context) http.Response {
|
||||
var request requests.UnArchive
|
||||
sanitize := SanitizeRequest(ctx, &request)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
if err := io.UnArchive(request.File, request.Path); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
r.setPermission(request.Path, 0755, "www", "www")
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Search
|
||||
//
|
||||
// @Summary 搜索文件/目录
|
||||
// @Description 通过关键词搜索给定路径的文件/目录
|
||||
// @Tags 文件
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body requests.Search true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/file/search [post]
|
||||
func (r *FileController) Search(ctx http.Context) http.Response {
|
||||
var request requests.Search
|
||||
sanitize := SanitizeRequest(ctx, &request)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
paths := make(map[string]stdos.FileInfo)
|
||||
err := filepath.Walk(request.Path, func(path string, info stdos.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.Contains(info.Name(), request.KeyWord) {
|
||||
paths[path] = info
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, paths)
|
||||
}
|
||||
|
||||
// List
|
||||
//
|
||||
// @Summary 获取文件/目录列表
|
||||
// @Description 获取给定路径的文件/目录列表
|
||||
// @Tags 文件
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data query requests.Exist true "request"
|
||||
// @Param data query commonrequests.Paginate true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/file/list [get]
|
||||
func (r *FileController) List(ctx http.Context) http.Response {
|
||||
var request requests.Exist
|
||||
sanitize := SanitizeRequest(ctx, &request)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
fileInfoList, err := io.ReadDir(request.Path)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
var paths []any
|
||||
for _, fileInfo := range fileInfoList {
|
||||
info, _ := fileInfo.Info()
|
||||
stat := info.Sys().(*syscall.Stat_t)
|
||||
|
||||
paths = append(paths, map[string]any{
|
||||
"name": info.Name(),
|
||||
"full": filepath.Join(request.Path, info.Name()),
|
||||
"size": str.FormatBytes(float64(info.Size())),
|
||||
"mode_str": info.Mode().String(),
|
||||
"mode": fmt.Sprintf("%04o", info.Mode().Perm()),
|
||||
"owner": os.GetUser(stat.Uid),
|
||||
"group": os.GetGroup(stat.Gid),
|
||||
"uid": stat.Uid,
|
||||
"gid": stat.Gid,
|
||||
"hidden": io.IsHidden(info.Name()),
|
||||
"symlink": io.IsSymlink(info.Mode()),
|
||||
"link": io.GetSymlink(filepath.Join(request.Path, info.Name())),
|
||||
"dir": info.IsDir(),
|
||||
"modify": carbon.FromStdTime(info.ModTime()).ToDateTimeString(),
|
||||
})
|
||||
}
|
||||
|
||||
paged, total := Paginate(ctx, paths)
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
}
|
||||
|
||||
// setPermission
|
||||
func (r *FileController) setPermission(path string, mode stdos.FileMode, owner, group string) {
|
||||
_ = io.Chmod(path, mode)
|
||||
_ = io.Chown(path, owner, group)
|
||||
}
|
||||
@@ -1,343 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"github.com/TheTNB/panel/app/models"
|
||||
"github.com/TheTNB/panel/internal"
|
||||
"github.com/TheTNB/panel/internal/services"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
"github.com/TheTNB/panel/pkg/tools"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type MenuItem struct {
|
||||
Name string `json:"name"`
|
||||
Title string `json:"title"`
|
||||
Icon string `json:"icon"`
|
||||
Jump string `json:"jump"`
|
||||
}
|
||||
|
||||
type InfoController struct {
|
||||
plugin internal.Plugin
|
||||
setting internal.Setting
|
||||
}
|
||||
|
||||
func NewInfoController() *InfoController {
|
||||
return &InfoController{
|
||||
plugin: services.NewPluginImpl(),
|
||||
setting: services.NewSettingImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// Panel 获取面板信息
|
||||
func (r *InfoController) Panel(ctx http.Context) http.Response {
|
||||
return Success(ctx, http.Json{
|
||||
"name": r.setting.Get(models.SettingKeyName),
|
||||
"language": facades.Config().GetString("app.locale"),
|
||||
})
|
||||
}
|
||||
|
||||
// HomePlugins 获取首页插件
|
||||
func (r *InfoController) HomePlugins(ctx http.Context) http.Response {
|
||||
var plugins []models.Plugin
|
||||
err := facades.Orm().Query().Where("show", 1).Find(&plugins)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "基础信息").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("获取首页插件失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
type pluginsData struct {
|
||||
models.Plugin
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
var pluginsJson []pluginsData
|
||||
for _, plugin := range plugins {
|
||||
pluginsJson = append(pluginsJson, pluginsData{
|
||||
Plugin: plugin,
|
||||
Name: r.plugin.GetBySlug(plugin.Slug).Name,
|
||||
})
|
||||
}
|
||||
|
||||
return Success(ctx, pluginsJson)
|
||||
}
|
||||
|
||||
// NowMonitor 获取当前监控信息
|
||||
func (r *InfoController) NowMonitor(ctx http.Context) http.Response {
|
||||
return Success(ctx, tools.GetMonitoringInfo())
|
||||
}
|
||||
|
||||
// SystemInfo 获取系统信息
|
||||
func (r *InfoController) SystemInfo(ctx http.Context) http.Response {
|
||||
monitorInfo := tools.GetMonitoringInfo()
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"os_name": monitorInfo.Host.Platform + " " + monitorInfo.Host.PlatformVersion,
|
||||
"uptime": fmt.Sprintf("%.2f", float64(monitorInfo.Host.Uptime)/86400),
|
||||
"panel_version": facades.Config().GetString("panel.version"),
|
||||
})
|
||||
}
|
||||
|
||||
// CountInfo 获取面板统计信息
|
||||
func (r *InfoController) CountInfo(ctx http.Context) http.Response {
|
||||
var websiteCount int64
|
||||
err := facades.Orm().Query().Model(models.Website{}).Count(&websiteCount)
|
||||
if err != nil {
|
||||
websiteCount = -1
|
||||
}
|
||||
|
||||
var mysql models.Plugin
|
||||
mysqlInstalled := true
|
||||
err = facades.Orm().Query().Where("slug like ?", "mysql%").FirstOrFail(&mysql)
|
||||
if err != nil {
|
||||
mysqlInstalled = false
|
||||
}
|
||||
var postgresql models.Plugin
|
||||
postgresqlInstalled := true
|
||||
err = facades.Orm().Query().Where("slug like ?", "postgresql%").FirstOrFail(&postgresql)
|
||||
if err != nil {
|
||||
postgresqlInstalled = false
|
||||
}
|
||||
var databaseCount int64
|
||||
if mysqlInstalled {
|
||||
status, err := systemctl.Status("mysqld")
|
||||
if status && err == nil {
|
||||
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
type database struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
db, err := sql.Open("mysql", "root:"+rootPassword+"@unix(/tmp/mysql.sock)/")
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "基础信息").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("获取数据库列表失败")
|
||||
databaseCount = -1
|
||||
} else {
|
||||
defer db.Close()
|
||||
rows, err := db.Query("SHOW DATABASES")
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "基础信息").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("获取数据库列表失败")
|
||||
databaseCount = -1
|
||||
} else {
|
||||
defer rows.Close()
|
||||
var databases []database
|
||||
for rows.Next() {
|
||||
var d database
|
||||
err := rows.Scan(&d.Name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if d.Name == "information_schema" || d.Name == "performance_schema" || d.Name == "mysql" || d.Name == "sys" {
|
||||
continue
|
||||
}
|
||||
|
||||
databases = append(databases, d)
|
||||
}
|
||||
databaseCount = int64(len(databases))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if postgresqlInstalled {
|
||||
status, err := systemctl.Status("postgresql")
|
||||
if status && err == nil {
|
||||
raw, err := shell.Execf(`echo "\l" | su - postgres -c "psql"`)
|
||||
if err == nil {
|
||||
databases := strings.Split(raw, "\n")
|
||||
if len(databases) >= 4 {
|
||||
databases = databases[3 : len(databases)-1]
|
||||
for _, db := range databases {
|
||||
parts := strings.Split(db, "|")
|
||||
if len(parts) != 9 || len(strings.TrimSpace(parts[0])) == 0 || strings.TrimSpace(parts[0]) == "template0" || strings.TrimSpace(parts[0]) == "template1" || strings.TrimSpace(parts[0]) == "postgres" {
|
||||
continue
|
||||
}
|
||||
|
||||
databaseCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var ftpCount int64
|
||||
var ftpPlugin = r.plugin.GetInstalledBySlug("pureftpd")
|
||||
if ftpPlugin.ID != 0 {
|
||||
listRaw, err := shell.Execf("pure-pw list")
|
||||
if len(listRaw) != 0 && err == nil {
|
||||
listArr := strings.Split(listRaw, "\n")
|
||||
ftpCount = int64(len(listArr))
|
||||
}
|
||||
}
|
||||
|
||||
var cronCount int64
|
||||
err = facades.Orm().Query().Model(models.Cron{}).Count(&cronCount)
|
||||
if err != nil {
|
||||
cronCount = -1
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"website": websiteCount,
|
||||
"database": databaseCount,
|
||||
"ftp": ftpCount,
|
||||
"cron": cronCount,
|
||||
})
|
||||
}
|
||||
|
||||
// InstalledDbAndPhp 获取已安装的数据库和 PHP 版本
|
||||
func (r *InfoController) InstalledDbAndPhp(ctx http.Context) http.Response {
|
||||
var php []models.Plugin
|
||||
err := facades.Orm().Query().Where("slug like ?", "php%").Find(&php)
|
||||
if err != nil {
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
var mysql models.Plugin
|
||||
mysqlInstalled := true
|
||||
err = facades.Orm().Query().Where("slug like ?", "mysql%").FirstOrFail(&mysql)
|
||||
if err != nil {
|
||||
mysqlInstalled = false
|
||||
}
|
||||
|
||||
var postgresql models.Plugin
|
||||
postgresqlInstalled := true
|
||||
err = facades.Orm().Query().Where("slug like ?", "postgresql%").FirstOrFail(&postgresql)
|
||||
if err != nil {
|
||||
postgresqlInstalled = false
|
||||
}
|
||||
|
||||
type data struct {
|
||||
Label string `json:"label"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
var phpData []data
|
||||
var dbData []data
|
||||
phpData = append(phpData, data{Value: "0", Label: "不使用"})
|
||||
dbData = append(dbData, data{Value: "0", Label: "不使用"})
|
||||
for _, p := range php {
|
||||
match := regexp.MustCompile(`php(\d+)`).FindStringSubmatch(p.Slug)
|
||||
if len(match) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
phpData = append(phpData, data{Value: strings.ReplaceAll(p.Slug, "php", ""), Label: r.plugin.GetBySlug(p.Slug).Name})
|
||||
}
|
||||
|
||||
if mysqlInstalled {
|
||||
dbData = append(dbData, data{Value: "mysql", Label: "MySQL"})
|
||||
}
|
||||
if postgresqlInstalled {
|
||||
dbData = append(dbData, data{Value: "postgresql", Label: "PostgreSQL"})
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"php": phpData,
|
||||
"db": dbData,
|
||||
})
|
||||
}
|
||||
|
||||
// CheckUpdate 检查面板更新
|
||||
func (r *InfoController) CheckUpdate(ctx http.Context) http.Response {
|
||||
version := facades.Config().GetString("panel.version")
|
||||
remote, err := tools.GetLatestPanelVersion()
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, "获取最新版本失败")
|
||||
}
|
||||
|
||||
if tools.VersionCompare(version, remote.Version, ">=") {
|
||||
return Success(ctx, http.Json{
|
||||
"update": false,
|
||||
})
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"update": true,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateInfo 获取更新信息
|
||||
func (r *InfoController) UpdateInfo(ctx http.Context) http.Response {
|
||||
version := facades.Config().GetString("panel.version")
|
||||
current, err := tools.GetLatestPanelVersion()
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, "获取最新版本失败")
|
||||
}
|
||||
|
||||
if tools.VersionCompare(version, current.Version, ">=") {
|
||||
return Error(ctx, http.StatusInternalServerError, "当前版本已是最新版本")
|
||||
}
|
||||
|
||||
versions, err := tools.GenerateVersions(version, current.Version)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, "获取更新信息失败")
|
||||
}
|
||||
|
||||
var versionInfo []tools.PanelInfo
|
||||
for _, v := range versions {
|
||||
info, err := tools.GetPanelVersion(v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
versionInfo = append(versionInfo, info)
|
||||
}
|
||||
|
||||
return Success(ctx, versionInfo)
|
||||
}
|
||||
|
||||
// Update 更新面板
|
||||
func (r *InfoController) Update(ctx http.Context) http.Response {
|
||||
var task models.Task
|
||||
if err := facades.Orm().Query().Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).FirstOrFail(&task); err == nil {
|
||||
return Error(ctx, http.StatusInternalServerError, "当前有任务正在执行,禁止更新")
|
||||
}
|
||||
if _, err := facades.Orm().Query().Exec("PRAGMA wal_checkpoint(TRUNCATE)"); err != nil {
|
||||
types.Status = types.StatusFailed
|
||||
return Error(ctx, http.StatusInternalServerError, fmt.Sprintf("面板数据库异常,已终止操作:%s", err.Error()))
|
||||
}
|
||||
|
||||
panel, err := tools.GetLatestPanelVersion()
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "基础信息").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("获取最新版本失败")
|
||||
return Error(ctx, http.StatusInternalServerError, "获取最新版本失败")
|
||||
}
|
||||
|
||||
types.Status = types.StatusUpgrade
|
||||
if err = tools.UpdatePanel(panel); err != nil {
|
||||
types.Status = types.StatusFailed
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "基础信息").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("更新面板失败")
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
types.Status = types.StatusNormal
|
||||
tools.RestartPanel()
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Restart 重启面板
|
||||
func (r *InfoController) Restart(ctx http.Context) http.Response {
|
||||
var task models.Task
|
||||
err := facades.Orm().Query().Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).FirstOrFail(&task)
|
||||
if err == nil {
|
||||
return Error(ctx, http.StatusInternalServerError, "当前有任务正在执行,禁止重启")
|
||||
}
|
||||
|
||||
tools.RestartPanel()
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/TheTNB/panel/app/models"
|
||||
"github.com/TheTNB/panel/internal"
|
||||
"github.com/TheTNB/panel/internal/services"
|
||||
)
|
||||
|
||||
type MonitorController struct {
|
||||
setting internal.Setting
|
||||
}
|
||||
|
||||
func NewMonitorController() *MonitorController {
|
||||
return &MonitorController{
|
||||
setting: services.NewSettingImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// Switch 监控开关
|
||||
func (r *MonitorController) Switch(ctx http.Context) http.Response {
|
||||
value := ctx.Request().InputBool("monitor")
|
||||
err := r.setting.Set(models.SettingKeyMonitor, cast.ToString(value))
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "资源监控").With(map[string]any{
|
||||
"monitor": value,
|
||||
"error": err.Error(),
|
||||
}).Info("更新监控开关失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// SaveDays 保存监控天数
|
||||
func (r *MonitorController) SaveDays(ctx http.Context) http.Response {
|
||||
days := ctx.Request().Input("days")
|
||||
err := r.setting.Set(models.SettingKeyMonitorDays, days)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "资源监控").With(map[string]any{
|
||||
"days": days,
|
||||
"error": err.Error(),
|
||||
}).Info("更新监控开关失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// SwitchAndDays 监控开关和监控天数
|
||||
func (r *MonitorController) SwitchAndDays(ctx http.Context) http.Response {
|
||||
monitor := r.setting.Get(models.SettingKeyMonitor)
|
||||
monitorDays := r.setting.Get(models.SettingKeyMonitorDays)
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"switch": cast.ToBool(monitor),
|
||||
"days": cast.ToInt(monitorDays),
|
||||
})
|
||||
}
|
||||
|
||||
// Clear 清空监控数据
|
||||
func (r *MonitorController) Clear(ctx http.Context) http.Response {
|
||||
_, err := facades.Orm().Query().Where("1 = 1").Delete(&models.Monitor{})
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "资源监控").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("清空监控数据失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// List 监控数据列表
|
||||
func (r *MonitorController) List(ctx http.Context) http.Response {
|
||||
start := ctx.Request().InputInt64("start")
|
||||
end := ctx.Request().InputInt64("end")
|
||||
startTime := carbon.FromTimestampMilli(start)
|
||||
endTime := carbon.FromTimestampMilli(end)
|
||||
|
||||
var monitors []models.Monitor
|
||||
err := facades.Orm().Query().Where("created_at >= ?", startTime.ToDateTimeString()).Where("created_at <= ?", endTime.ToDateTimeString()).Get(&monitors)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "资源监控").With(map[string]any{
|
||||
"start": startTime.ToDateTimeString(),
|
||||
"end": endTime.ToDateTimeString(),
|
||||
"error": err.Error(),
|
||||
}).Info("获取监控数据失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
if len(monitors) == 0 {
|
||||
return Error(ctx, http.StatusNotFound, "监控数据为空")
|
||||
}
|
||||
|
||||
type load struct {
|
||||
Load1 []float64 `json:"load1"`
|
||||
Load5 []float64 `json:"load5"`
|
||||
Load15 []float64 `json:"load15"`
|
||||
}
|
||||
type cpu struct {
|
||||
Percent []string `json:"percent"`
|
||||
}
|
||||
type mem struct {
|
||||
Total string `json:"total"`
|
||||
Available []string `json:"available"`
|
||||
Used []string `json:"used"`
|
||||
}
|
||||
type swap struct {
|
||||
Total string `json:"total"`
|
||||
Used []string `json:"used"`
|
||||
Free []string `json:"free"`
|
||||
}
|
||||
type network struct {
|
||||
Sent []string `json:"sent"`
|
||||
Recv []string `json:"recv"`
|
||||
Tx []string `json:"tx"`
|
||||
Rx []string `json:"rx"`
|
||||
}
|
||||
type monitorData struct {
|
||||
Times []string `json:"times"`
|
||||
Load load `json:"load"`
|
||||
Cpu cpu `json:"cpu"`
|
||||
Mem mem `json:"mem"`
|
||||
Swap swap `json:"swap"`
|
||||
Net network `json:"net"`
|
||||
}
|
||||
|
||||
var data monitorData
|
||||
var bytesSent uint64
|
||||
var bytesRecv uint64
|
||||
var bytesSent2 uint64
|
||||
var bytesRecv2 uint64
|
||||
for _, net := range monitors[0].Info.Net {
|
||||
if net.Name == "lo" {
|
||||
continue
|
||||
}
|
||||
bytesSent += net.BytesSent
|
||||
bytesRecv += net.BytesRecv
|
||||
}
|
||||
for i, monitor := range monitors {
|
||||
// 跳过第一条数据,因为第一条数据的流量为 0
|
||||
if i == 0 {
|
||||
// MB
|
||||
data.Mem.Total = fmt.Sprintf("%.2f", float64(monitor.Info.Mem.Total)/1024/1024)
|
||||
data.Swap.Total = fmt.Sprintf("%.2f", float64(monitor.Info.Swap.Total)/1024/1024)
|
||||
continue
|
||||
}
|
||||
for _, net := range monitor.Info.Net {
|
||||
if net.Name == "lo" {
|
||||
continue
|
||||
}
|
||||
bytesSent2 += net.BytesSent
|
||||
bytesRecv2 += net.BytesRecv
|
||||
}
|
||||
data.Times = append(data.Times, monitor.CreatedAt.ToDateTimeString())
|
||||
data.Load.Load1 = append(data.Load.Load1, monitor.Info.Load.Load1)
|
||||
data.Load.Load5 = append(data.Load.Load5, monitor.Info.Load.Load5)
|
||||
data.Load.Load15 = append(data.Load.Load15, monitor.Info.Load.Load15)
|
||||
data.Cpu.Percent = append(data.Cpu.Percent, fmt.Sprintf("%.2f", monitor.Info.Percent[0]))
|
||||
data.Mem.Available = append(data.Mem.Available, fmt.Sprintf("%.2f", float64(monitor.Info.Mem.Available)/1024/1024))
|
||||
data.Mem.Used = append(data.Mem.Used, fmt.Sprintf("%.2f", float64(monitor.Info.Mem.Used)/1024/1024))
|
||||
data.Swap.Used = append(data.Swap.Used, fmt.Sprintf("%.2f", float64(monitor.Info.Swap.Used)/1024/1024))
|
||||
data.Swap.Free = append(data.Swap.Free, fmt.Sprintf("%.2f", float64(monitor.Info.Swap.Free)/1024/1024))
|
||||
data.Net.Sent = append(data.Net.Sent, fmt.Sprintf("%.2f", float64(bytesSent2/1024/1024)))
|
||||
data.Net.Recv = append(data.Net.Recv, fmt.Sprintf("%.2f", float64(bytesRecv2/1024/1024)))
|
||||
|
||||
// 监控频率为 1 分钟,所以这里除以 60 即可得到每秒的流量
|
||||
data.Net.Tx = append(data.Net.Tx, fmt.Sprintf("%.2f", float64(bytesSent2-bytesSent)/60/1024/1024))
|
||||
data.Net.Rx = append(data.Net.Rx, fmt.Sprintf("%.2f", float64(bytesRecv2-bytesRecv)/60/1024/1024))
|
||||
|
||||
bytesSent = bytesSent2
|
||||
bytesRecv = bytesRecv2
|
||||
bytesSent2 = 0
|
||||
bytesRecv2 = 0
|
||||
}
|
||||
|
||||
return Success(ctx, data)
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"github.com/TheTNB/panel/app/models"
|
||||
"github.com/TheTNB/panel/internal"
|
||||
"github.com/TheTNB/panel/internal/services"
|
||||
)
|
||||
|
||||
type PluginController struct {
|
||||
plugin internal.Plugin
|
||||
task internal.Task
|
||||
}
|
||||
|
||||
func NewPluginController() *PluginController {
|
||||
return &PluginController{
|
||||
plugin: services.NewPluginImpl(),
|
||||
task: services.NewTaskImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// List
|
||||
//
|
||||
// @Summary 插件列表
|
||||
// @Tags 插件
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /panel/plugin/list [get]
|
||||
func (r *PluginController) List(ctx http.Context) http.Response {
|
||||
plugins := r.plugin.All()
|
||||
installedPlugins, err := r.plugin.AllInstalled()
|
||||
if err != nil {
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
installedPluginsMap := make(map[string]models.Plugin)
|
||||
|
||||
for _, p := range installedPlugins {
|
||||
installedPluginsMap[p.Slug] = p
|
||||
}
|
||||
|
||||
type plugin struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Slug string `json:"slug"`
|
||||
Version string `json:"version"`
|
||||
Requires []string `json:"requires"`
|
||||
Excludes []string `json:"excludes"`
|
||||
Installed bool `json:"installed"`
|
||||
InstalledVersion string `json:"installed_version"`
|
||||
Show bool `json:"show"`
|
||||
}
|
||||
|
||||
var pluginArr []plugin
|
||||
for _, item := range plugins {
|
||||
installed, installedVersion, show := false, "", false
|
||||
if _, ok := installedPluginsMap[item.Slug]; ok {
|
||||
installed = true
|
||||
installedVersion = installedPluginsMap[item.Slug].Version
|
||||
show = installedPluginsMap[item.Slug].Show
|
||||
}
|
||||
pluginArr = append(pluginArr, plugin{
|
||||
Name: item.Name,
|
||||
Description: item.Description,
|
||||
Slug: item.Slug,
|
||||
Version: item.Version,
|
||||
Requires: item.Requires,
|
||||
Excludes: item.Excludes,
|
||||
Installed: installed,
|
||||
InstalledVersion: installedVersion,
|
||||
Show: show,
|
||||
})
|
||||
}
|
||||
|
||||
paged, total := Paginate(ctx, pluginArr)
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
}
|
||||
|
||||
// Install
|
||||
//
|
||||
// @Summary 安装插件
|
||||
// @Tags 插件
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param slug query string true "request"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /panel/plugin/install [post]
|
||||
func (r *PluginController) Install(ctx http.Context) http.Response {
|
||||
slug := ctx.Request().Input("slug")
|
||||
|
||||
if err := r.plugin.Install(slug); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, "任务已提交")
|
||||
}
|
||||
|
||||
// Uninstall
|
||||
//
|
||||
// @Summary 卸载插件
|
||||
// @Tags 插件
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param slug query string true "request"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /panel/plugin/uninstall [post]
|
||||
func (r *PluginController) Uninstall(ctx http.Context) http.Response {
|
||||
slug := ctx.Request().Input("slug")
|
||||
|
||||
if err := r.plugin.Uninstall(slug); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, "任务已提交")
|
||||
}
|
||||
|
||||
// Update
|
||||
//
|
||||
// @Summary 更新插件
|
||||
// @Tags 插件
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param slug query string true "request"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /panel/plugin/update [post]
|
||||
func (r *PluginController) Update(ctx http.Context) http.Response {
|
||||
slug := ctx.Request().Input("slug")
|
||||
|
||||
if err := r.plugin.Update(slug); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, "任务已提交")
|
||||
}
|
||||
|
||||
// UpdateShow
|
||||
//
|
||||
// @Summary 更新插件首页显示状态
|
||||
// @Tags 插件
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param slug query string true "request"
|
||||
// @Param show query bool true "request"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /panel/plugin/updateShow [post]
|
||||
func (r *PluginController) UpdateShow(ctx http.Context) http.Response {
|
||||
slug := ctx.Request().Input("slug")
|
||||
show := ctx.Request().InputBool("show")
|
||||
|
||||
var plugin models.Plugin
|
||||
if err := facades.Orm().Query().Where("slug", slug).First(&plugin); err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "插件中心").With(map[string]any{
|
||||
"slug": slug,
|
||||
"err": err.Error(),
|
||||
}).Info("获取插件失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
if plugin.ID == 0 {
|
||||
return Error(ctx, http.StatusUnprocessableEntity, "插件未安装")
|
||||
}
|
||||
|
||||
plugin.Show = show
|
||||
if err := facades.Orm().Query().Save(&plugin); err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "插件中心").With(map[string]any{
|
||||
"slug": slug,
|
||||
"err": err.Error(),
|
||||
}).Info("更新插件失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, "操作成功")
|
||||
}
|
||||
|
||||
// IsInstalled
|
||||
//
|
||||
// @Summary 检查插件是否已安装
|
||||
// @Tags 插件
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param slug query string true "request"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /panel/plugin/isInstalled [get]
|
||||
func (r *PluginController) IsInstalled(ctx http.Context) http.Response {
|
||||
slug := ctx.Request().Input("slug")
|
||||
|
||||
plugin := r.plugin.GetInstalledBySlug(slug)
|
||||
info := r.plugin.GetBySlug(slug)
|
||||
if plugin.Slug != slug {
|
||||
return Success(ctx, http.Json{
|
||||
"name": info.Name,
|
||||
"installed": false,
|
||||
})
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"name": info.Name,
|
||||
"installed": true,
|
||||
})
|
||||
}
|
||||
@@ -1,342 +0,0 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/TheTNB/panel/app/http/controllers"
|
||||
"github.com/TheTNB/panel/app/models"
|
||||
"github.com/TheTNB/panel/internal"
|
||||
"github.com/TheTNB/panel/internal/services"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/os"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type Fail2banController struct {
|
||||
website internal.Website
|
||||
}
|
||||
|
||||
func NewFail2banController() *Fail2banController {
|
||||
return &Fail2banController{
|
||||
website: services.NewWebsiteImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// List 所有 Fail2ban 规则
|
||||
func (r *Fail2banController) List(ctx http.Context) http.Response {
|
||||
raw, err := io.Read("/etc/fail2ban/jail.local")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
|
||||
}
|
||||
|
||||
jailList := regexp.MustCompile(`\[(.*?)]`).FindAllStringSubmatch(raw, -1)
|
||||
if len(jailList) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "Fail2ban 规则为空")
|
||||
}
|
||||
|
||||
var jails []types.Fail2banJail
|
||||
for i, jail := range jailList {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
jailName := jail[1]
|
||||
jailRaw := str.Cut(raw, "# "+jailName+"-START", "# "+jailName+"-END")
|
||||
if len(jailRaw) == 0 {
|
||||
continue
|
||||
}
|
||||
jailEnabled := strings.Contains(jailRaw, "enabled = true")
|
||||
jailLogPath := regexp.MustCompile(`logpath = (.*)`).FindStringSubmatch(jailRaw)
|
||||
jailMaxRetry := regexp.MustCompile(`maxretry = (.*)`).FindStringSubmatch(jailRaw)
|
||||
jailFindTime := regexp.MustCompile(`findtime = (.*)`).FindStringSubmatch(jailRaw)
|
||||
jailBanTime := regexp.MustCompile(`bantime = (.*)`).FindStringSubmatch(jailRaw)
|
||||
|
||||
jails = append(jails, types.Fail2banJail{
|
||||
Name: jailName,
|
||||
Enabled: jailEnabled,
|
||||
LogPath: jailLogPath[1],
|
||||
MaxRetry: cast.ToInt(jailMaxRetry[1]),
|
||||
FindTime: cast.ToInt(jailFindTime[1]),
|
||||
BanTime: cast.ToInt(jailBanTime[1]),
|
||||
})
|
||||
}
|
||||
|
||||
paged, total := controllers.Paginate(ctx, jails)
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
}
|
||||
|
||||
// Add 添加 Fail2ban 规则
|
||||
func (r *Fail2banController) Add(ctx http.Context) http.Response {
|
||||
if sanitize := controllers.Sanitize(ctx, map[string]string{
|
||||
"name": "required",
|
||||
"type": "required|in:website,service",
|
||||
"maxretry": "required",
|
||||
"findtime": "required",
|
||||
"bantime": "required",
|
||||
"website_name": "required_if:type,website",
|
||||
"website_mode": "required_if:type,website",
|
||||
"website_path": "required_if:website_mode,path",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
jailName := ctx.Request().Input("name")
|
||||
jailType := ctx.Request().Input("type")
|
||||
jailMaxRetry := ctx.Request().Input("maxretry")
|
||||
jailFindTime := ctx.Request().Input("findtime")
|
||||
jailBanTime := ctx.Request().Input("bantime")
|
||||
jailWebsiteName := ctx.Request().Input("website_name")
|
||||
jailWebsiteMode := ctx.Request().Input("website_mode")
|
||||
jailWebsitePath := ctx.Request().Input("website_path")
|
||||
|
||||
raw, err := io.Read("/etc/fail2ban/jail.local")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
|
||||
}
|
||||
if (strings.Contains(raw, "["+jailName+"]") && jailType == "service") || (strings.Contains(raw, "["+jailWebsiteName+"]"+"-cc") && jailType == "website" && jailWebsiteMode == "cc") || (strings.Contains(raw, "["+jailWebsiteName+"]"+"-path") && jailType == "website" && jailWebsiteMode == "path") {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "规则已存在")
|
||||
}
|
||||
|
||||
switch jailType {
|
||||
case "website":
|
||||
var website models.Website
|
||||
err := facades.Orm().Query().Where("name", jailWebsiteName).FirstOrFail(&website)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "网站不存在")
|
||||
}
|
||||
config, err := r.website.GetConfig(website.ID)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "获取网站配置失败")
|
||||
}
|
||||
var ports string
|
||||
for _, port := range config.Ports {
|
||||
fields := strings.Fields(cast.ToString(port))
|
||||
ports += fields[0] + ","
|
||||
}
|
||||
|
||||
rule := `
|
||||
# ` + jailWebsiteName + `-` + jailWebsiteMode + `-START
|
||||
[` + jailWebsiteName + `-` + jailWebsiteMode + `]
|
||||
enabled = true
|
||||
filter = haozi-` + jailWebsiteName + `-` + jailWebsiteMode + `
|
||||
port = ` + ports + `
|
||||
maxretry = ` + jailMaxRetry + `
|
||||
findtime = ` + jailFindTime + `
|
||||
bantime = ` + jailBanTime + `
|
||||
action = %(action_mwl)s
|
||||
logpath = /www/wwwlogs/` + website.Name + `.log
|
||||
# ` + jailWebsiteName + `-` + jailWebsiteMode + `-END
|
||||
`
|
||||
raw += rule
|
||||
if err = io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "写入Fail2ban规则失败")
|
||||
}
|
||||
|
||||
var filter string
|
||||
if jailWebsiteMode == "cc" {
|
||||
filter = `
|
||||
[Definition]
|
||||
failregex = ^<HOST>\s-.*HTTP/.*$
|
||||
ignoreregex =
|
||||
`
|
||||
} else {
|
||||
filter = `
|
||||
[Definition]
|
||||
failregex = ^<HOST>\s-.*\s` + jailWebsitePath + `.*HTTP/.*$
|
||||
ignoreregex =
|
||||
`
|
||||
}
|
||||
if err = io.Write("/etc/fail2ban/filter.d/haozi-"+jailWebsiteName+"-"+jailWebsiteMode+".conf", filter, 0644); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "写入Fail2ban规则失败")
|
||||
}
|
||||
|
||||
case "service":
|
||||
var logPath string
|
||||
var filter string
|
||||
var port string
|
||||
var err error
|
||||
switch jailName {
|
||||
case "ssh":
|
||||
if os.IsDebian() {
|
||||
logPath = "/var/log/auth.log"
|
||||
} else {
|
||||
logPath = "/var/log/secure"
|
||||
}
|
||||
filter = "sshd"
|
||||
port, err = shell.Execf("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'")
|
||||
case "mysql":
|
||||
logPath = "/www/server/mysql/mysql-error.log"
|
||||
filter = "mysqld-auth"
|
||||
port, err = shell.Execf("cat /www/server/mysql/conf/my.cnf | grep 'port' | head -n 1 | awk '{print $3}'")
|
||||
case "pure-ftpd":
|
||||
logPath = "/var/log/messages"
|
||||
filter = "pure-ftpd"
|
||||
port, err = shell.Execf(`cat /www/server/pure-ftpd/etc/pure-ftpd.conf | grep "Bind" | awk '{print $2}' | awk -F "," '{print $2}'`)
|
||||
default:
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "未知服务")
|
||||
}
|
||||
if len(port) == 0 || err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "获取服务端口失败,请检查是否安装")
|
||||
}
|
||||
|
||||
rule := `
|
||||
# ` + jailName + `-START
|
||||
[` + jailName + `]
|
||||
enabled = true
|
||||
filter = ` + filter + `
|
||||
port = ` + port + `
|
||||
maxretry = ` + jailMaxRetry + `
|
||||
findtime = ` + jailFindTime + `
|
||||
bantime = ` + jailBanTime + `
|
||||
action = %(action_mwl)s
|
||||
logpath = ` + logPath + `
|
||||
# ` + jailName + `-END
|
||||
`
|
||||
raw += rule
|
||||
if err := io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "写入Fail2ban规则失败")
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := shell.Execf("fail2ban-client reload"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "重载配置失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Delete 删除规则
|
||||
func (r *Fail2banController) Delete(ctx http.Context) http.Response {
|
||||
jailName := ctx.Request().Input("name")
|
||||
raw, err := io.Read("/etc/fail2ban/jail.local")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
|
||||
}
|
||||
if !strings.Contains(raw, "["+jailName+"]") {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "规则不存在")
|
||||
}
|
||||
|
||||
rule := str.Cut(raw, "# "+jailName+"-START", "# "+jailName+"-END")
|
||||
raw = strings.Replace(raw, "\n# "+jailName+"-START"+rule+"# "+jailName+"-END", "", -1)
|
||||
raw = strings.TrimSpace(raw)
|
||||
if err := io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "写入Fail2ban规则失败")
|
||||
}
|
||||
|
||||
if _, err := shell.Execf("fail2ban-client reload"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "重载配置失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// BanList 获取封禁列表
|
||||
func (r *Fail2banController) BanList(ctx http.Context) http.Response {
|
||||
name := ctx.Request().Input("name")
|
||||
if len(name) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "缺少参数")
|
||||
}
|
||||
|
||||
currentlyBan, err := shell.Execf(`fail2ban-client status %s | grep "Currently banned" | awk '{print $4}'`, name)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取封禁列表失败")
|
||||
}
|
||||
totalBan, err := shell.Execf(`fail2ban-client status %s | grep "Total banned" | awk '{print $4}'`, name)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取封禁列表失败")
|
||||
}
|
||||
bannedIp, err := shell.Execf(`fail2ban-client status %s | grep "Banned IP list" | awk -F ":" '{print $2}'`, name)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取封禁列表失败")
|
||||
}
|
||||
bannedIpList := strings.Split(bannedIp, " ")
|
||||
|
||||
var list []map[string]string
|
||||
for _, ip := range bannedIpList {
|
||||
if len(ip) > 0 {
|
||||
list = append(list, map[string]string{
|
||||
"name": name,
|
||||
"ip": ip,
|
||||
})
|
||||
}
|
||||
}
|
||||
if list == nil {
|
||||
list = []map[string]string{}
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"currently_ban": currentlyBan,
|
||||
"total_ban": totalBan,
|
||||
"baned_list": list,
|
||||
})
|
||||
}
|
||||
|
||||
// Unban 解封
|
||||
func (r *Fail2banController) Unban(ctx http.Context) http.Response {
|
||||
name := ctx.Request().Input("name")
|
||||
ip := ctx.Request().Input("ip")
|
||||
if len(name) == 0 || len(ip) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "缺少参数")
|
||||
}
|
||||
|
||||
if _, err := shell.Execf("fail2ban-client set %s unbanip %s", name, ip); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "解封失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// SetWhiteList 设置白名单
|
||||
func (r *Fail2banController) SetWhiteList(ctx http.Context) http.Response {
|
||||
ip := ctx.Request().Input("ip")
|
||||
if len(ip) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "缺少参数")
|
||||
}
|
||||
|
||||
raw, err := io.Read("/etc/fail2ban/jail.local")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
|
||||
}
|
||||
// 正则替换
|
||||
reg := regexp.MustCompile(`ignoreip\s*=\s*.*\n`)
|
||||
if reg.MatchString(raw) {
|
||||
raw = reg.ReplaceAllString(raw, "ignoreip = "+ip+"\n")
|
||||
} else {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "解析Fail2ban规则失败,Fail2ban可能已损坏")
|
||||
}
|
||||
|
||||
if err := io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "写入Fail2ban规则失败")
|
||||
}
|
||||
|
||||
if _, err := shell.Execf("fail2ban-client reload"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "重载配置失败")
|
||||
}
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetWhiteList 获取白名单
|
||||
func (r *Fail2banController) GetWhiteList(ctx http.Context) http.Response {
|
||||
raw, err := io.Read("/etc/fail2ban/jail.local")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
|
||||
}
|
||||
reg := regexp.MustCompile(`ignoreip\s*=\s*(.*)\n`)
|
||||
if reg.MatchString(raw) {
|
||||
ignoreIp := reg.FindStringSubmatch(raw)[1]
|
||||
return controllers.Success(ctx, ignoreIp)
|
||||
} else {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "解析Fail2ban规则失败,Fail2ban可能已损坏")
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
|
||||
"github.com/TheTNB/panel/app/http/controllers"
|
||||
requests "github.com/TheTNB/panel/app/http/requests/plugins/frp"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
)
|
||||
|
||||
type FrpController struct {
|
||||
}
|
||||
|
||||
func NewFrpController() *FrpController {
|
||||
return &FrpController{}
|
||||
}
|
||||
|
||||
// GetConfig
|
||||
//
|
||||
// @Summary 获取配置
|
||||
// @Description 获取 Frp 配置
|
||||
// @Tags 插件-Frp
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param service query string false "服务"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/frp/config [get]
|
||||
func (r *FrpController) GetConfig(ctx http.Context) http.Response {
|
||||
var serviceRequest requests.Service
|
||||
sanitize := controllers.SanitizeRequest(ctx, &serviceRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
config, err := io.Read(fmt.Sprintf("/www/server/frp/%s.toml", serviceRequest.Service))
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// UpdateConfig
|
||||
//
|
||||
// @Summary 更新配置
|
||||
// @Description 更新 Frp 配置
|
||||
// @Tags 插件-Frp
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.UpdateConfig true "request"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/frp/config [post]
|
||||
func (r *FrpController) UpdateConfig(ctx http.Context) http.Response {
|
||||
var updateRequest requests.UpdateConfig
|
||||
sanitize := controllers.SanitizeRequest(ctx, &updateRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
if err := io.Write(fmt.Sprintf("/www/server/frp/%s.toml", updateRequest.Service), updateRequest.Config, 0644); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if err := systemctl.Restart(updateRequest.Service); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
|
||||
"github.com/TheTNB/panel/app/http/controllers"
|
||||
requests "github.com/TheTNB/panel/app/http/requests/plugins/gitea"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
)
|
||||
|
||||
type GiteaController struct {
|
||||
}
|
||||
|
||||
func NewGiteaController() *GiteaController {
|
||||
return &GiteaController{}
|
||||
}
|
||||
|
||||
// GetConfig
|
||||
//
|
||||
// @Summary 获取配置
|
||||
// @Description 获取 Gitea 配置
|
||||
// @Tags 插件-Gitea
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/gitea/config [get]
|
||||
func (r *GiteaController) GetConfig(ctx http.Context) http.Response {
|
||||
config, err := io.Read("/www/server/gitea/app.ini")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// UpdateConfig
|
||||
//
|
||||
// @Summary 更新配置
|
||||
// @Description 更新 Gitea 配置
|
||||
// @Tags 插件-Gitea
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.UpdateConfig true "request"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/gitea/config [post]
|
||||
func (r *GiteaController) UpdateConfig(ctx http.Context) http.Response {
|
||||
var updateRequest requests.UpdateConfig
|
||||
sanitize := controllers.SanitizeRequest(ctx, &updateRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
if err := io.Write("/www/server/gitea/app.ini", updateRequest.Config, 0644); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if err := systemctl.Restart("gitea"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -1,506 +0,0 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/TheTNB/panel/app/http/controllers"
|
||||
"github.com/TheTNB/panel/app/models"
|
||||
"github.com/TheTNB/panel/internal"
|
||||
"github.com/TheTNB/panel/internal/services"
|
||||
"github.com/TheTNB/panel/pkg/db"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type MySQLController struct {
|
||||
setting internal.Setting
|
||||
backup internal.Backup
|
||||
}
|
||||
|
||||
func NewMySQLController() *MySQLController {
|
||||
return &MySQLController{
|
||||
setting: services.NewSettingImpl(),
|
||||
backup: services.NewBackupImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (r *MySQLController) GetConfig(ctx http.Context) http.Response {
|
||||
config, err := io.Read("/www/server/mysql/conf/my.cnf")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL配置失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveConfig 保存配置
|
||||
func (r *MySQLController) SaveConfig(ctx http.Context) http.Response {
|
||||
config := ctx.Request().Input("config")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "配置不能为空")
|
||||
}
|
||||
|
||||
if err := io.Write("/www/server/mysql/conf/my.cnf", config, 0644); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "写入MySQL配置失败")
|
||||
}
|
||||
|
||||
if err := systemctl.Reload("mysqld"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "重载MySQL失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Load 获取负载
|
||||
func (r *MySQLController) Load(ctx http.Context) http.Response {
|
||||
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
if len(rootPassword) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "MySQL root密码为空")
|
||||
}
|
||||
|
||||
status, _ := systemctl.Status("mysqld")
|
||||
if !status {
|
||||
return controllers.Success(ctx, []types.NV{})
|
||||
}
|
||||
|
||||
raw, err := shell.Execf("mysqladmin -uroot -p" + rootPassword + " extended-status 2>&1")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL负载失败")
|
||||
}
|
||||
|
||||
var data []map[string]string
|
||||
expressions := []struct {
|
||||
regex string
|
||||
name string
|
||||
}{
|
||||
{`Uptime\s+\|\s+(\d+)\s+\|`, "运行时间"},
|
||||
{`Queries\s+\|\s+(\d+)\s+\|`, "总查询次数"},
|
||||
{`Connections\s+\|\s+(\d+)\s+\|`, "总连接次数"},
|
||||
{`Com_commit\s+\|\s+(\d+)\s+\|`, "每秒事务"},
|
||||
{`Com_rollback\s+\|\s+(\d+)\s+\|`, "每秒回滚"},
|
||||
{`Bytes_sent\s+\|\s+(\d+)\s+\|`, "发送"},
|
||||
{`Bytes_received\s+\|\s+(\d+)\s+\|`, "接收"},
|
||||
{`Threads_connected\s+\|\s+(\d+)\s+\|`, "活动连接数"},
|
||||
{`Max_used_connections\s+\|\s+(\d+)\s+\|`, "峰值连接数"},
|
||||
{`Key_read_requests\s+\|\s+(\d+)\s+\|`, "索引命中率"},
|
||||
{`Innodb_buffer_pool_reads\s+\|\s+(\d+)\s+\|`, "Innodb索引命中率"},
|
||||
{`Created_tmp_disk_tables\s+\|\s+(\d+)\s+\|`, "创建临时表到磁盘"},
|
||||
{`Open_tables\s+\|\s+(\d+)\s+\|`, "已打开的表"},
|
||||
{`Select_full_join\s+\|\s+(\d+)\s+\|`, "没有使用索引的量"},
|
||||
{`Select_full_range_join\s+\|\s+(\d+)\s+\|`, "没有索引的JOIN量"},
|
||||
{`Select_range_check\s+\|\s+(\d+)\s+\|`, "没有索引的子查询量"},
|
||||
{`Sort_merge_passes\s+\|\s+(\d+)\s+\|`, "排序后的合并次数"},
|
||||
{`Table_locks_waited\s+\|\s+(\d+)\s+\|`, "锁表次数"},
|
||||
}
|
||||
|
||||
for _, expression := range expressions {
|
||||
re := regexp.MustCompile(expression.regex)
|
||||
matches := re.FindStringSubmatch(raw)
|
||||
if len(matches) > 1 {
|
||||
d := map[string]string{"name": expression.name, "value": matches[1]}
|
||||
if expression.name == "发送" || expression.name == "接收" {
|
||||
d["value"] = str.FormatBytes(cast.ToFloat64(matches[1]))
|
||||
}
|
||||
|
||||
data = append(data, d)
|
||||
}
|
||||
}
|
||||
|
||||
// 索引命中率
|
||||
readRequests := cast.ToFloat64(data[9]["value"])
|
||||
reads := cast.ToFloat64(data[10]["value"])
|
||||
data[9]["value"] = fmt.Sprintf("%.2f%%", readRequests/(reads+readRequests)*100)
|
||||
// Innodb 索引命中率
|
||||
bufferPoolReads := cast.ToFloat64(data[11]["value"])
|
||||
bufferPoolReadRequests := cast.ToFloat64(data[12]["value"])
|
||||
data[10]["value"] = fmt.Sprintf("%.2f%%", bufferPoolReadRequests/(bufferPoolReads+bufferPoolReadRequests)*100)
|
||||
|
||||
return controllers.Success(ctx, data)
|
||||
}
|
||||
|
||||
// ErrorLog 获取错误日志
|
||||
func (r *MySQLController) ErrorLog(ctx http.Context) http.Response {
|
||||
log, err := shell.Execf("tail -n 100 /www/server/mysql/mysql-error.log")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, log)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
// ClearErrorLog 清空错误日志
|
||||
func (r *MySQLController) ClearErrorLog(ctx http.Context) http.Response {
|
||||
if out, err := shell.Execf("echo '' > /www/server/mysql/mysql-error.log"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// SlowLog 获取慢查询日志
|
||||
func (r *MySQLController) SlowLog(ctx http.Context) http.Response {
|
||||
log, err := shell.Execf("tail -n 100 /www/server/mysql/mysql-slow.log")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, log)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
// ClearSlowLog 清空慢查询日志
|
||||
func (r *MySQLController) ClearSlowLog(ctx http.Context) http.Response {
|
||||
if out, err := shell.Execf("echo '' > /www/server/mysql/mysql-slow.log"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetRootPassword 获取root密码
|
||||
func (r *MySQLController) GetRootPassword(ctx http.Context) http.Response {
|
||||
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
if len(rootPassword) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "MySQL root密码为空")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, rootPassword)
|
||||
}
|
||||
|
||||
// SetRootPassword 设置root密码
|
||||
func (r *MySQLController) SetRootPassword(ctx http.Context) http.Response {
|
||||
rootPassword := ctx.Request().Input("password")
|
||||
if len(rootPassword) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "MySQL root密码不能为空")
|
||||
}
|
||||
|
||||
oldRootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
mysql, err := db.NewMySQL("root", oldRootPassword, r.getSock(), "unix")
|
||||
if err != nil {
|
||||
// 尝试安全模式直接改密
|
||||
if err = db.MySQLResetRootPassword(rootPassword); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
} else {
|
||||
if err = mysql.UserPassword("root", rootPassword); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
}
|
||||
if err = r.setting.Set(models.SettingKeyMysqlRootPassword, rootPassword); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("设置保存失败: %v", err))
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// DatabaseList 获取数据库列表
|
||||
func (r *MySQLController) DatabaseList(ctx http.Context) http.Response {
|
||||
password := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
mysql, err := db.NewMySQL("root", password, r.getSock(), "unix")
|
||||
if err != nil {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []types.MySQLDatabase{},
|
||||
})
|
||||
}
|
||||
|
||||
databases, err := mysql.Databases()
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取数据库列表失败")
|
||||
}
|
||||
paged, total := controllers.Paginate(ctx, databases)
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
}
|
||||
|
||||
// AddDatabase 添加数据库
|
||||
func (r *MySQLController) AddDatabase(ctx http.Context) http.Response {
|
||||
if sanitize := controllers.Sanitize(ctx, map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"password": "required|min_len:8|max_len:255",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
database := ctx.Request().Input("database")
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
|
||||
mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if err = mysql.DatabaseCreate(database); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if err = mysql.UserCreate(user, password); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if err = mysql.PrivilegesGrant(user, database); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// DeleteDatabase 删除数据库
|
||||
func (r *MySQLController) DeleteDatabase(ctx http.Context) http.Response {
|
||||
if sanitize := controllers.Sanitize(ctx, map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
database := ctx.Request().Input("database")
|
||||
mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if err = mysql.DatabaseDrop(database); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// BackupList 获取备份列表
|
||||
func (r *MySQLController) BackupList(ctx http.Context) http.Response {
|
||||
backups, err := r.backup.MysqlList()
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
paged, total := controllers.Paginate(ctx, backups)
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
}
|
||||
|
||||
// UploadBackup 上传备份
|
||||
func (r *MySQLController) UploadBackup(ctx http.Context) http.Response {
|
||||
file, err := ctx.Request().File("file")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败")
|
||||
}
|
||||
|
||||
backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/mysql"
|
||||
if !io.Exists(backupPath) {
|
||||
if err = io.Mkdir(backupPath, 0644); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
name := file.GetClientOriginalName()
|
||||
_, err = file.StoreAs(backupPath, name)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// CreateBackup 创建备份
|
||||
func (r *MySQLController) CreateBackup(ctx http.Context) http.Response {
|
||||
if sanitize := controllers.Sanitize(ctx, map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
database := ctx.Request().Input("database")
|
||||
if err := r.backup.MysqlBackup(database); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// DeleteBackup 删除备份
|
||||
func (r *MySQLController) DeleteBackup(ctx http.Context) http.Response {
|
||||
if sanitize := controllers.Sanitize(ctx, map[string]string{
|
||||
"name": "required|min_len:1|max_len:255",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/mysql"
|
||||
fileName := ctx.Request().Input("name")
|
||||
if err := io.Remove(backupPath + "/" + fileName); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// RestoreBackup 还原备份
|
||||
func (r *MySQLController) RestoreBackup(ctx http.Context) http.Response {
|
||||
if sanitize := controllers.Sanitize(ctx, map[string]string{
|
||||
"backup": "required|min_len:1|max_len:255",
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
if err := r.backup.MysqlRestore(ctx.Request().Input("database"), ctx.Request().Input("backup")); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// UserList 用户列表
|
||||
func (r *MySQLController) UserList(ctx http.Context) http.Response {
|
||||
password := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
mysql, err := db.NewMySQL("root", password, r.getSock(), "unix")
|
||||
if err != nil {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []types.MySQLUser{},
|
||||
})
|
||||
}
|
||||
|
||||
users, err := mysql.Users()
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取用户列表失败")
|
||||
}
|
||||
paged, total := controllers.Paginate(ctx, users)
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
}
|
||||
|
||||
// AddUser 添加用户
|
||||
func (r *MySQLController) AddUser(ctx http.Context) http.Response {
|
||||
if sanitize := controllers.Sanitize(ctx, map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"password": "required|min_len:8|max_len:255",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
database := ctx.Request().Input("database")
|
||||
mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if err = mysql.UserCreate(user, password); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if err = mysql.PrivilegesGrant(user, database); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// DeleteUser 删除用户
|
||||
func (r *MySQLController) DeleteUser(ctx http.Context) http.Response {
|
||||
if sanitize := controllers.Sanitize(ctx, map[string]string{
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
user := ctx.Request().Input("user")
|
||||
mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if err = mysql.UserDrop(user); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// SetUserPassword 设置用户密码
|
||||
func (r *MySQLController) SetUserPassword(ctx http.Context) http.Response {
|
||||
if sanitize := controllers.Sanitize(ctx, map[string]string{
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"password": "required|min_len:8|max_len:255",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if err = mysql.UserPassword(user, password); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// SetUserPrivileges 设置用户权限
|
||||
func (r *MySQLController) SetUserPrivileges(ctx http.Context) http.Response {
|
||||
if sanitize := controllers.Sanitize(ctx, map[string]string{
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"database": "required|min_len:1|max_len:255",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
user := ctx.Request().Input("user")
|
||||
database := ctx.Request().Input("database")
|
||||
mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if err = mysql.PrivilegesGrant(user, database); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// getSock 获取sock文件位置
|
||||
func (r *MySQLController) getSock() string {
|
||||
if io.Exists("/tmp/mysql.sock") {
|
||||
return "/tmp/mysql.sock"
|
||||
}
|
||||
if io.Exists("/www/server/mysql/config/my.cnf") {
|
||||
config, _ := io.Read("/www/server/mysql/config/my.cnf")
|
||||
re := regexp.MustCompile(`socket\s*=\s*(['"]?)([^'"]+)`)
|
||||
matches := re.FindStringSubmatch(config)
|
||||
if len(matches) > 2 {
|
||||
return matches[2]
|
||||
}
|
||||
}
|
||||
if io.Exists("/etc/my.cnf") {
|
||||
config, _ := io.Read("/etc/my.cnf")
|
||||
re := regexp.MustCompile(`socket\s*=\s*(['"]?)([^'"]+)`)
|
||||
matches := re.FindStringSubmatch(config)
|
||||
if len(matches) > 2 {
|
||||
return matches[2]
|
||||
}
|
||||
}
|
||||
|
||||
return "/tmp/mysql.sock"
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/TheTNB/panel/app/http/controllers"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type OpenRestyController struct {
|
||||
// Dependent services
|
||||
}
|
||||
|
||||
func NewOpenrestyController() *OpenRestyController {
|
||||
return &OpenRestyController{}
|
||||
}
|
||||
|
||||
// GetConfig
|
||||
//
|
||||
// @Summary 获取配置
|
||||
// @Tags 插件-OpenResty
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/openresty/config [get]
|
||||
func (r *OpenRestyController) GetConfig(ctx http.Context) http.Response {
|
||||
config, err := io.Read("/www/server/openresty/conf/nginx.conf")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取配置失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveConfig
|
||||
//
|
||||
// @Summary 保存配置
|
||||
// @Tags 插件-OpenResty
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param config body string true "配置"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/openresty/config [post]
|
||||
func (r *OpenRestyController) SaveConfig(ctx http.Context) http.Response {
|
||||
config := ctx.Request().Input("config")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "配置不能为空")
|
||||
}
|
||||
|
||||
if err := io.Write("/www/server/openresty/conf/nginx.conf", config, 0644); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "保存配置失败")
|
||||
}
|
||||
|
||||
if err := systemctl.Reload("openresty"); err != nil {
|
||||
_, err = shell.Execf("openresty -t")
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载服务失败: %v", err))
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// ErrorLog
|
||||
//
|
||||
// @Summary 获取错误日志
|
||||
// @Tags 插件-OpenResty
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/openresty/errorLog [get]
|
||||
func (r *OpenRestyController) ErrorLog(ctx http.Context) http.Response {
|
||||
if !io.Exists("/www/wwwlogs/nginx_error.log") {
|
||||
return controllers.Success(ctx, "")
|
||||
}
|
||||
|
||||
out, err := shell.Execf("tail -n 100 /www/wwwlogs/openresty_error.log")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, out)
|
||||
}
|
||||
|
||||
// ClearErrorLog
|
||||
//
|
||||
// @Summary 清空错误日志
|
||||
// @Tags 插件-OpenResty
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/openresty/clearErrorLog [post]
|
||||
func (r *OpenRestyController) ClearErrorLog(ctx http.Context) http.Response {
|
||||
if out, err := shell.Execf("echo '' > /www/wwwlogs/openresty_error.log"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Load
|
||||
//
|
||||
// @Summary 获取负载状态
|
||||
// @Tags 插件-OpenResty
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/openresty/load [get]
|
||||
func (r *OpenRestyController) Load(ctx http.Context) http.Response {
|
||||
client := resty.New().SetTimeout(10 * time.Second)
|
||||
resp, err := client.R().Get("http://127.0.0.1/nginx_status")
|
||||
if err != nil || !resp.IsSuccess() {
|
||||
return controllers.Success(ctx, []types.NV{})
|
||||
}
|
||||
|
||||
raw := resp.String()
|
||||
var data []types.NV
|
||||
|
||||
workers, err := shell.Execf("ps aux | grep nginx | grep 'worker process' | wc -l")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取负载失败")
|
||||
}
|
||||
data = append(data, types.NV{
|
||||
Name: "工作进程",
|
||||
Value: workers,
|
||||
})
|
||||
|
||||
out, err := shell.Execf("ps aux | grep nginx | grep 'worker process' | awk '{memsum+=$6};END {print memsum}'")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取负载失败")
|
||||
}
|
||||
mem := str.FormatBytes(cast.ToFloat64(out))
|
||||
data = append(data, types.NV{
|
||||
Name: "内存占用",
|
||||
Value: mem,
|
||||
})
|
||||
|
||||
match := regexp.MustCompile(`Active connections:\s+(\d+)`).FindStringSubmatch(raw)
|
||||
if len(match) == 2 {
|
||||
data = append(data, types.NV{
|
||||
Name: "活跃连接数",
|
||||
Value: match[1],
|
||||
})
|
||||
}
|
||||
|
||||
match = regexp.MustCompile(`server accepts handled requests\s+(\d+)\s+(\d+)\s+(\d+)`).FindStringSubmatch(raw)
|
||||
if len(match) == 4 {
|
||||
data = append(data, types.NV{
|
||||
Name: "总连接次数",
|
||||
Value: match[1],
|
||||
})
|
||||
data = append(data, types.NV{
|
||||
Name: "总握手次数",
|
||||
Value: match[2],
|
||||
})
|
||||
data = append(data, types.NV{
|
||||
Name: "总请求次数",
|
||||
Value: match[3],
|
||||
})
|
||||
}
|
||||
|
||||
match = regexp.MustCompile(`Reading:\s+(\d+)\s+Writing:\s+(\d+)\s+Waiting:\s+(\d+)`).FindStringSubmatch(raw)
|
||||
if len(match) == 4 {
|
||||
data = append(data, types.NV{
|
||||
Name: "请求数",
|
||||
Value: match[1],
|
||||
})
|
||||
data = append(data, types.NV{
|
||||
Name: "响应数",
|
||||
Value: match[2],
|
||||
})
|
||||
data = append(data, types.NV{
|
||||
Name: "驻留进程",
|
||||
Value: match[3],
|
||||
})
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, data)
|
||||
}
|
||||
@@ -1,246 +0,0 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
|
||||
"github.com/TheTNB/panel/app/http/controllers"
|
||||
"github.com/TheTNB/panel/internal/services"
|
||||
)
|
||||
|
||||
type PHPController struct{}
|
||||
|
||||
func NewPHPController() *PHPController {
|
||||
return &PHPController{}
|
||||
}
|
||||
|
||||
// GetConfig
|
||||
//
|
||||
// @Summary 获取配置
|
||||
// @Tags 插件-PHP
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param version path int true "PHP 版本"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/php/{version}/config [get]
|
||||
func (r *PHPController) GetConfig(ctx http.Context) http.Response {
|
||||
service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version")))
|
||||
config, err := service.GetConfig()
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveConfig
|
||||
//
|
||||
// @Summary 保存配置
|
||||
// @Tags 插件-PHP
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param version path int true "PHP 版本"
|
||||
// @Param config body string true "配置"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/php/{version}/config [post]
|
||||
func (r *PHPController) SaveConfig(ctx http.Context) http.Response {
|
||||
service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version")))
|
||||
config := ctx.Request().Input("config")
|
||||
if err := service.SaveConfig(config); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetFPMConfig
|
||||
//
|
||||
// @Summary 获取 FPM 配置
|
||||
// @Tags 插件-PHP
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param version path int true "PHP 版本"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/php/{version}/fpmConfig [get]
|
||||
func (r *PHPController) GetFPMConfig(ctx http.Context) http.Response {
|
||||
service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version")))
|
||||
config, err := service.GetFPMConfig()
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveFPMConfig
|
||||
//
|
||||
// @Summary 保存 FPM 配置
|
||||
// @Tags 插件-PHP
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param version path int true "PHP 版本"
|
||||
// @Param config body string true "配置"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/php/{version}/fpmConfig [post]
|
||||
func (r *PHPController) SaveFPMConfig(ctx http.Context) http.Response {
|
||||
service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version")))
|
||||
config := ctx.Request().Input("config")
|
||||
if err := service.SaveFPMConfig(config); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Load
|
||||
//
|
||||
// @Summary 获取负载状态
|
||||
// @Tags 插件-PHP
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param version path int true "PHP 版本"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/php/{version}/load [get]
|
||||
func (r *PHPController) Load(ctx http.Context) http.Response {
|
||||
service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version")))
|
||||
load, err := service.Load()
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, load)
|
||||
}
|
||||
|
||||
// ErrorLog
|
||||
//
|
||||
// @Summary 获取错误日志
|
||||
// @Tags 插件-PHP
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param version path int true "PHP 版本"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/php/{version}/errorLog [get]
|
||||
func (r *PHPController) ErrorLog(ctx http.Context) http.Response {
|
||||
service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version")))
|
||||
log, _ := service.GetErrorLog()
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
// SlowLog
|
||||
//
|
||||
// @Summary 获取慢日志
|
||||
// @Tags 插件-PHP
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param version path int true "PHP 版本"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/php/{version}/slowLog [get]
|
||||
func (r *PHPController) SlowLog(ctx http.Context) http.Response {
|
||||
service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version")))
|
||||
log, _ := service.GetSlowLog()
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
// ClearErrorLog
|
||||
//
|
||||
// @Summary 清空错误日志
|
||||
// @Tags 插件-PHP
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param version path int true "PHP 版本"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/php/{version}/clearErrorLog [post]
|
||||
func (r *PHPController) ClearErrorLog(ctx http.Context) http.Response {
|
||||
service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version")))
|
||||
err := service.ClearErrorLog()
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// ClearSlowLog
|
||||
//
|
||||
// @Summary 清空慢日志
|
||||
// @Tags 插件-PHP
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param version path int true "PHP 版本"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/php/{version}/clearSlowLog [post]
|
||||
func (r *PHPController) ClearSlowLog(ctx http.Context) http.Response {
|
||||
service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version")))
|
||||
err := service.ClearSlowLog()
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// ExtensionList
|
||||
//
|
||||
// @Summary 获取扩展列表
|
||||
// @Tags 插件-PHP
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param version path int true "PHP 版本"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/php/{version}/extensions [get]
|
||||
func (r *PHPController) ExtensionList(ctx http.Context) http.Response {
|
||||
service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version")))
|
||||
extensions, err := service.GetExtensions()
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, extensions)
|
||||
}
|
||||
|
||||
// InstallExtension
|
||||
//
|
||||
// @Summary 安装扩展
|
||||
// @Tags 插件-PHP
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param version path int true "PHP 版本"
|
||||
// @Param slug query string true "slug"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/php/{version}/extensions [post]
|
||||
func (r *PHPController) InstallExtension(ctx http.Context) http.Response {
|
||||
service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version")))
|
||||
slug := ctx.Request().Input("slug")
|
||||
if len(slug) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "参数错误")
|
||||
}
|
||||
|
||||
if err := service.InstallExtension(slug); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// UninstallExtension
|
||||
//
|
||||
// @Summary 卸载扩展
|
||||
// @Tags 插件-PHP
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param version path int true "PHP 版本"
|
||||
// @Param slug query string true "slug"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/php/{version}/extensions [delete]
|
||||
func (r *PHPController) UninstallExtension(ctx http.Context) http.Response {
|
||||
service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version")))
|
||||
slug := ctx.Request().Input("slug")
|
||||
if len(slug) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "参数错误")
|
||||
}
|
||||
|
||||
if err := service.UninstallExtension(slug); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/TheTNB/panel/app/http/controllers"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/os"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
)
|
||||
|
||||
type PhpMyAdminController struct {
|
||||
}
|
||||
|
||||
func NewPhpMyAdminController() *PhpMyAdminController {
|
||||
return &PhpMyAdminController{}
|
||||
}
|
||||
|
||||
func (r *PhpMyAdminController) Info(ctx http.Context) http.Response {
|
||||
files, err := io.ReadDir("/www/server/phpmyadmin")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "找不到 phpMyAdmin 目录")
|
||||
}
|
||||
|
||||
var phpmyadmin string
|
||||
for _, f := range files {
|
||||
if strings.HasPrefix(f.Name(), "phpmyadmin_") {
|
||||
phpmyadmin = f.Name()
|
||||
}
|
||||
}
|
||||
if len(phpmyadmin) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "找不到 phpMyAdmin 目录")
|
||||
}
|
||||
|
||||
conf, err := io.Read("/www/server/vhost/phpmyadmin.conf")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
match := regexp.MustCompile(`listen\s+(\d+);`).FindStringSubmatch(conf)
|
||||
if len(match) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "找不到 phpMyAdmin 端口")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"path": phpmyadmin,
|
||||
"port": cast.ToInt(match[1]),
|
||||
})
|
||||
}
|
||||
|
||||
func (r *PhpMyAdminController) SetPort(ctx http.Context) http.Response {
|
||||
port := ctx.Request().InputInt("port")
|
||||
if port == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "端口不能为空")
|
||||
}
|
||||
|
||||
conf, err := io.Read("/www/server/vhost/phpmyadmin.conf")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
conf = regexp.MustCompile(`listen\s+(\d+);`).ReplaceAllString(conf, "listen "+cast.ToString(port)+";")
|
||||
if err := io.Write("/www/server/vhost/phpmyadmin.conf", conf, 0644); err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("插件", "phpMyAdmin").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("修改 phpMyAdmin 端口失败")
|
||||
return controllers.ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
if os.IsRHEL() {
|
||||
if out, err := shell.Execf("firewall-cmd --zone=public --add-port=%d/tcp --permanent", port); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if out, err := shell.Execf("firewall-cmd --reload"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
} else {
|
||||
if out, err := shell.Execf("ufw allow %d/tcp", port); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if out, err := shell.Execf("ufw reload"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
}
|
||||
|
||||
if err = systemctl.Reload("openresty"); err != nil {
|
||||
_, err = shell.Execf("openresty -t")
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载OpenResty失败: %v", err))
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *PhpMyAdminController) GetConfig(ctx http.Context) http.Response {
|
||||
config, err := io.Read("/www/server/vhost/phpmyadmin.conf")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
func (r *PhpMyAdminController) SaveConfig(ctx http.Context) http.Response {
|
||||
config := ctx.Request().Input("config")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "配置不能为空")
|
||||
}
|
||||
|
||||
if err := io.Write("/www/server/vhost/phpmyadmin.conf", config, 0644); err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("插件", "phpMyAdmin").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("修改 phpMyAdmin 配置失败")
|
||||
return controllers.ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
if err := systemctl.Reload("openresty"); err != nil {
|
||||
_, err = shell.Execf("openresty -t")
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载OpenResty失败: %v", err))
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
|
||||
"github.com/TheTNB/panel/app/http/controllers"
|
||||
requests "github.com/TheTNB/panel/app/http/requests/plugins/podman"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
)
|
||||
|
||||
type PodmanController struct {
|
||||
}
|
||||
|
||||
func NewPodmanController() *PodmanController {
|
||||
return &PodmanController{}
|
||||
}
|
||||
|
||||
// GetRegistryConfig
|
||||
//
|
||||
// @Summary 获取注册表配置
|
||||
// @Description 获取 Podman 注册表配置
|
||||
// @Tags 插件-Podman
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/podman/registryConfig [get]
|
||||
func (r *PodmanController) GetRegistryConfig(ctx http.Context) http.Response {
|
||||
config, err := io.Read("/etc/containers/registries.conf")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// UpdateRegistryConfig
|
||||
//
|
||||
// @Summary 更新注册表配置
|
||||
// @Description 更新 Podman 注册表配置
|
||||
// @Tags 插件-Podman
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.UpdateRegistryConfig true "request"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/podman/registryConfig [post]
|
||||
func (r *PodmanController) UpdateRegistryConfig(ctx http.Context) http.Response {
|
||||
var updateRequest requests.UpdateRegistryConfig
|
||||
sanitize := controllers.SanitizeRequest(ctx, &updateRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
if err := io.Write("/etc/containers/registries.conf", updateRequest.Config, 0644); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if err := systemctl.Restart("podman"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetStorageConfig
|
||||
//
|
||||
// @Summary 获取存储配置
|
||||
// @Description 获取 Podman 存储配置
|
||||
// @Tags 插件-Podman
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/podman/storageConfig [get]
|
||||
func (r *PodmanController) GetStorageConfig(ctx http.Context) http.Response {
|
||||
config, err := io.Read("/etc/containers/storage.conf")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// UpdateStorageConfig
|
||||
//
|
||||
// @Summary 更新存储配置
|
||||
// @Description 更新 Podman 存储配置
|
||||
// @Tags 插件-Podman
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.UpdateStorageConfig true "request"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/podman/storageConfig [post]
|
||||
func (r *PodmanController) UpdateStorageConfig(ctx http.Context) http.Response {
|
||||
var updateRequest requests.UpdateStorageConfig
|
||||
sanitize := controllers.SanitizeRequest(ctx, &updateRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
if err := io.Write("/etc/containers/storage.conf", updateRequest.Config, 0644); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if err := systemctl.Restart("podman"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -1,491 +0,0 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
_ "github.com/lib/pq"
|
||||
|
||||
"github.com/TheTNB/panel/app/http/controllers"
|
||||
"github.com/TheTNB/panel/app/models"
|
||||
"github.com/TheTNB/panel/internal"
|
||||
"github.com/TheTNB/panel/internal/services"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type PostgreSQLController struct {
|
||||
setting internal.Setting
|
||||
backup internal.Backup
|
||||
}
|
||||
|
||||
func NewPostgreSQLController() *PostgreSQLController {
|
||||
return &PostgreSQLController{
|
||||
setting: services.NewSettingImpl(),
|
||||
backup: services.NewBackupImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (r *PostgreSQLController) GetConfig(ctx http.Context) http.Response {
|
||||
// 获取配置
|
||||
config, err := io.Read("/www/server/postgresql/data/postgresql.conf")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL配置失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// GetUserConfig 获取用户配置
|
||||
func (r *PostgreSQLController) GetUserConfig(ctx http.Context) http.Response {
|
||||
// 获取配置
|
||||
config, err := io.Read("/www/server/postgresql/data/pg_hba.conf")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL配置失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveConfig 保存配置
|
||||
func (r *PostgreSQLController) SaveConfig(ctx http.Context) http.Response {
|
||||
config := ctx.Request().Input("config")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "配置不能为空")
|
||||
}
|
||||
|
||||
if err := io.Write("/www/server/postgresql/data/postgresql.conf", config, 0644); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "写入PostgreSQL配置失败")
|
||||
}
|
||||
|
||||
if err := systemctl.Reload("postgresql"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "重载服务失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// SaveUserConfig 保存用户配置
|
||||
func (r *PostgreSQLController) SaveUserConfig(ctx http.Context) http.Response {
|
||||
config := ctx.Request().Input("config")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "配置不能为空")
|
||||
}
|
||||
|
||||
if err := io.Write("/www/server/postgresql/data/pg_hba.conf", config, 0644); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "写入PostgreSQL配置失败")
|
||||
}
|
||||
|
||||
if err := systemctl.Reload("postgresql"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "重载服务失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Load 获取负载
|
||||
func (r *PostgreSQLController) Load(ctx http.Context) http.Response {
|
||||
status, _ := systemctl.Status("postgresql")
|
||||
if !status {
|
||||
return controllers.Success(ctx, []types.NV{})
|
||||
}
|
||||
|
||||
time, err := shell.Execf(`echo "select pg_postmaster_start_time();" | su - postgres -c "psql" | sed -n 3p | cut -d'.' -f1`)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL启动时间失败")
|
||||
}
|
||||
pid, err := shell.Execf(`echo "select pg_backend_pid();" | su - postgres -c "psql" | sed -n 3p`)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL进程PID失败")
|
||||
}
|
||||
process, err := shell.Execf(`ps aux | grep postgres | grep -v grep | wc -l`)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL进程数失败")
|
||||
}
|
||||
connections, err := shell.Execf(`echo "SELECT count(*) FROM pg_stat_activity WHERE NOT pid=pg_backend_pid();" | su - postgres -c "psql" | sed -n 3p`)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL连接数失败")
|
||||
}
|
||||
storage, err := shell.Execf(`echo "select pg_size_pretty(pg_database_size('postgres'));" | su - postgres -c "psql" | sed -n 3p`)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL空间占用失败")
|
||||
}
|
||||
|
||||
data := []types.NV{
|
||||
{Name: "启动时间", Value: carbon.Parse(time).ToDateTimeString()},
|
||||
{Name: "进程 PID", Value: pid},
|
||||
{Name: "进程数", Value: process},
|
||||
{Name: "总连接数", Value: connections},
|
||||
{Name: "空间占用", Value: storage},
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, data)
|
||||
}
|
||||
|
||||
// Log 获取日志
|
||||
func (r *PostgreSQLController) Log(ctx http.Context) http.Response {
|
||||
log, err := shell.Execf("tail -n 100 /www/server/postgresql/logs/postgresql-" + carbon.Now().ToDateString() + ".log")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, log)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
// ClearLog 清空日志
|
||||
func (r *PostgreSQLController) ClearLog(ctx http.Context) http.Response {
|
||||
if out, err := shell.Execf("echo '' > /www/server/postgresql/logs/postgresql-" + carbon.Now().ToDateString() + ".log"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// DatabaseList 获取数据库列表
|
||||
func (r *PostgreSQLController) DatabaseList(ctx http.Context) http.Response {
|
||||
type database struct {
|
||||
Name string `json:"name"`
|
||||
Owner string `json:"owner"`
|
||||
Encoding string `json:"encoding"`
|
||||
}
|
||||
|
||||
db, err := sql.Open("postgres", "host=localhost port=5432 user=postgres dbname=postgres sslmode=disable")
|
||||
if err != nil {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []database{},
|
||||
})
|
||||
}
|
||||
|
||||
if err = db.Ping(); err != nil {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []database{},
|
||||
})
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT d.datname, pg_catalog.pg_get_userbyid(d.datdba), pg_catalog.pg_encoding_to_char(d.encoding)
|
||||
FROM pg_catalog.pg_database d
|
||||
WHERE datistemplate = false;
|
||||
`
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var databases []database
|
||||
for rows.Next() {
|
||||
var db database
|
||||
if err := rows.Scan(&db.Name, &db.Owner, &db.Encoding); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
databases = append(databases, db)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
paged, total := controllers.Paginate(ctx, databases)
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
}
|
||||
|
||||
// AddDatabase 添加数据库
|
||||
func (r *PostgreSQLController) AddDatabase(ctx http.Context) http.Response {
|
||||
if sanitize := controllers.Sanitize(ctx, map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"password": "required|min_len:8|max_len:255",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
database := ctx.Request().Input("database")
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
|
||||
if out, err := shell.Execf(`echo "CREATE DATABASE ` + database + `;" | su - postgres -c "psql"`); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if out, err := shell.Execf(`echo "CREATE USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if out, err := shell.Execf(`echo "ALTER DATABASE ` + database + ` OWNER TO ` + user + `;" | su - postgres -c "psql"`); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if out, err := shell.Execf(`echo "GRANT ALL PRIVILEGES ON DATABASE ` + database + ` TO ` + user + `;" | su - postgres -c "psql"`); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
userConfig := "host " + database + " " + user + " 127.0.0.1/32 scram-sha-256"
|
||||
if out, err := shell.Execf(`echo "` + userConfig + `" >> /www/server/postgresql/data/pg_hba.conf`); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
if err := systemctl.Reload("postgresql"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "重载服务失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// DeleteDatabase 删除数据库
|
||||
func (r *PostgreSQLController) DeleteDatabase(ctx http.Context) http.Response {
|
||||
if sanitize := controllers.Sanitize(ctx, map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:postgres,template0,template1",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
database := ctx.Request().Input("database")
|
||||
if out, err := shell.Execf(`echo "DROP DATABASE ` + database + `;" | su - postgres -c "psql"`); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// BackupList 获取备份列表
|
||||
func (r *PostgreSQLController) BackupList(ctx http.Context) http.Response {
|
||||
backups, err := r.backup.PostgresqlList()
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取备份列表失败")
|
||||
}
|
||||
|
||||
paged, total := controllers.Paginate(ctx, backups)
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
}
|
||||
|
||||
// UploadBackup 上传备份
|
||||
func (r *PostgreSQLController) UploadBackup(ctx http.Context) http.Response {
|
||||
file, err := ctx.Request().File("file")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败")
|
||||
}
|
||||
|
||||
backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/postgresql"
|
||||
if !io.Exists(backupPath) {
|
||||
if err = io.Mkdir(backupPath, 0644); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
name := file.GetClientOriginalName()
|
||||
_, err = file.StoreAs(backupPath, name)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// CreateBackup 创建备份
|
||||
func (r *PostgreSQLController) CreateBackup(ctx http.Context) http.Response {
|
||||
if sanitize := controllers.Sanitize(ctx, map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
database := ctx.Request().Input("database")
|
||||
if err := r.backup.PostgresqlBackup(database); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// DeleteBackup 删除备份
|
||||
func (r *PostgreSQLController) DeleteBackup(ctx http.Context) http.Response {
|
||||
if sanitize := controllers.Sanitize(ctx, map[string]string{
|
||||
"name": "required|min_len:1|max_len:255",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/postgresql"
|
||||
fileName := ctx.Request().Input("name")
|
||||
if err := io.Remove(backupPath + "/" + fileName); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// RestoreBackup 还原备份
|
||||
func (r *PostgreSQLController) RestoreBackup(ctx http.Context) http.Response {
|
||||
if sanitize := controllers.Sanitize(ctx, map[string]string{
|
||||
"backup": "required|min_len:1|max_len:255",
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
if err := r.backup.PostgresqlRestore(ctx.Request().Input("database"), ctx.Request().Input("backup")); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "还原失败: "+err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// RoleList 角色列表
|
||||
func (r *PostgreSQLController) RoleList(ctx http.Context) http.Response {
|
||||
type role struct {
|
||||
Role string `json:"role"`
|
||||
Attributes []string `json:"attributes"`
|
||||
}
|
||||
|
||||
db, err := sql.Open("postgres", "host=localhost port=5432 user=postgres dbname=postgres sslmode=disable")
|
||||
if err != nil {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []role{},
|
||||
})
|
||||
}
|
||||
if err = db.Ping(); err != nil {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []role{},
|
||||
})
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT rolname,
|
||||
rolsuper,
|
||||
rolcreaterole,
|
||||
rolcreatedb,
|
||||
rolreplication,
|
||||
rolbypassrls
|
||||
FROM pg_roles
|
||||
WHERE rolcanlogin = true;
|
||||
`
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var roles []role
|
||||
for rows.Next() {
|
||||
var r role
|
||||
var super, canCreateRole, canCreateDb, replication, bypassRls bool
|
||||
if err = rows.Scan(&r.Role, &super, &canCreateRole, &canCreateDb, &replication, &bypassRls); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
permissions := map[string]bool{
|
||||
"超级用户": super,
|
||||
"创建角色": canCreateRole,
|
||||
"创建数据库": canCreateDb,
|
||||
"可以复制": replication,
|
||||
"绕过行级安全": bypassRls,
|
||||
}
|
||||
for perm, enabled := range permissions {
|
||||
if enabled {
|
||||
r.Attributes = append(r.Attributes, perm)
|
||||
}
|
||||
}
|
||||
|
||||
if len(r.Attributes) == 0 {
|
||||
r.Attributes = append(r.Attributes, "无")
|
||||
}
|
||||
|
||||
roles = append(roles, r)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
paged, total := controllers.Paginate(ctx, roles)
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
}
|
||||
|
||||
// AddRole 添加角色
|
||||
func (r *PostgreSQLController) AddRole(ctx http.Context) http.Response {
|
||||
if sanitize := controllers.Sanitize(ctx, map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"password": "required|min_len:8|max_len:255",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
database := ctx.Request().Input("database")
|
||||
if out, err := shell.Execf(`echo "CREATE USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if out, err := shell.Execf(`echo "GRANT ALL PRIVILEGES ON DATABASE ` + database + ` TO ` + user + `;" | su - postgres -c "psql"`); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
userConfig := "host " + database + " " + user + " 127.0.0.1/32 scram-sha-256"
|
||||
if out, err := shell.Execf(`echo "` + userConfig + `" >> /www/server/postgresql/data/pg_hba.conf`); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
if err := systemctl.Reload("postgresql"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "重载服务失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// DeleteRole 删除角色
|
||||
func (r *PostgreSQLController) DeleteRole(ctx http.Context) http.Response {
|
||||
if sanitize := controllers.Sanitize(ctx, map[string]string{
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
user := ctx.Request().Input("user")
|
||||
if out, err := shell.Execf(`echo "DROP USER ` + user + `;" | su - postgres -c "psql"`); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if out, err := shell.Execf(`sed -i '/` + user + `/d' /www/server/postgresql/data/pg_hba.conf`); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
if err := systemctl.Reload("postgresql"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "重载服务失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// SetRolePassword 设置用户密码
|
||||
func (r *PostgreSQLController) SetRolePassword(ctx http.Context) http.Response {
|
||||
if sanitize := controllers.Sanitize(ctx, map[string]string{
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"password": "required|min_len:8|max_len:255",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
if out, err := shell.Execf(`echo "ALTER USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/TheTNB/panel/app/http/controllers"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/os"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type PureFtpdController struct {
|
||||
}
|
||||
|
||||
func NewPureFtpdController() *PureFtpdController {
|
||||
return &PureFtpdController{}
|
||||
}
|
||||
|
||||
// List 获取用户列表
|
||||
func (r *PureFtpdController) List(ctx http.Context) http.Response {
|
||||
listRaw, err := shell.Execf("pure-pw list")
|
||||
if err != nil {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []types.PureFtpdUser{},
|
||||
})
|
||||
}
|
||||
|
||||
listArr := strings.Split(listRaw, "\n")
|
||||
var users []types.PureFtpdUser
|
||||
for _, v := range listArr {
|
||||
if len(v) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
match := regexp.MustCompile(`(\S+)\s+(\S+)`).FindStringSubmatch(v)
|
||||
users = append(users, types.PureFtpdUser{
|
||||
Username: match[1],
|
||||
Path: strings.Replace(match[2], "/./", "/", 1),
|
||||
})
|
||||
}
|
||||
|
||||
paged, total := controllers.Paginate(ctx, users)
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
}
|
||||
|
||||
// Add 添加用户
|
||||
func (r *PureFtpdController) Add(ctx http.Context) http.Response {
|
||||
if sanitize := controllers.Sanitize(ctx, map[string]string{
|
||||
"username": "required",
|
||||
"password": "required|min_len:6",
|
||||
"path": "required",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
username := ctx.Request().Input("username")
|
||||
password := ctx.Request().Input("password")
|
||||
path := ctx.Request().Input("path")
|
||||
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
if !io.Exists(path) {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "目录不存在")
|
||||
}
|
||||
|
||||
if err := io.Chmod(path, 0755); err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "修改目录权限失败")
|
||||
}
|
||||
if err := io.Chown(path, "www", "www"); err != nil {
|
||||
return nil
|
||||
}
|
||||
if out, err := shell.Execf(`yes '` + password + `' | pure-pw useradd ` + username + ` -u www -g www -d ` + path); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if out, err := shell.Execf("pure-pw mkdb"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Delete 删除用户
|
||||
func (r *PureFtpdController) Delete(ctx http.Context) http.Response {
|
||||
if sanitize := controllers.Sanitize(ctx, map[string]string{
|
||||
"username": "required",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
username := ctx.Request().Input("username")
|
||||
|
||||
if out, err := shell.Execf("pure-pw userdel " + username + " -m"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if out, err := shell.Execf("pure-pw mkdb"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// ChangePassword 修改密码
|
||||
func (r *PureFtpdController) ChangePassword(ctx http.Context) http.Response {
|
||||
if sanitize := controllers.Sanitize(ctx, map[string]string{
|
||||
"username": "required",
|
||||
"password": "required|min_len:6",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
username := ctx.Request().Input("username")
|
||||
password := ctx.Request().Input("password")
|
||||
|
||||
if out, err := shell.Execf(`yes '` + password + `' | pure-pw passwd ` + username + ` -m`); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if out, err := shell.Execf("pure-pw mkdb"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetPort 获取端口
|
||||
func (r *PureFtpdController) GetPort(ctx http.Context) http.Response {
|
||||
port, err := shell.Execf(`cat /www/server/pure-ftpd/etc/pure-ftpd.conf | grep "Bind" | awk '{print $2}' | awk -F "," '{print $2}'`)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PureFtpd端口失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, cast.ToInt(port))
|
||||
}
|
||||
|
||||
// SetPort 设置端口
|
||||
func (r *PureFtpdController) SetPort(ctx http.Context) http.Response {
|
||||
if sanitize := controllers.Sanitize(ctx, map[string]string{
|
||||
"port": "required",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
port := ctx.Request().Input("port")
|
||||
if out, err := shell.Execf(`sed -i "s/Bind.*/Bind 0.0.0.0,%s/g" /www/server/pure-ftpd/etc/pure-ftpd.conf`, port); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if os.IsRHEL() {
|
||||
if out, err := shell.Execf("firewall-cmd --zone=public --add-port=%s/tcp --permanent", port); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if out, err := shell.Execf("firewall-cmd --reload"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
} else {
|
||||
if out, err := shell.Execf("ufw allow %s/tcp", port); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if out, err := shell.Execf("ufw reload"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
}
|
||||
|
||||
if err := systemctl.Restart("pure-ftpd"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -1,299 +0,0 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
|
||||
"github.com/TheTNB/panel/app/http/controllers"
|
||||
requests "github.com/TheTNB/panel/app/http/requests/plugins/rsync"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type RsyncController struct {
|
||||
}
|
||||
|
||||
func NewRsyncController() *RsyncController {
|
||||
return &RsyncController{}
|
||||
}
|
||||
|
||||
// List
|
||||
//
|
||||
// @Summary 列出模块
|
||||
// @Description 列出所有 Rsync 模块
|
||||
// @Tags 插件-Rsync
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data query commonrequests.Paginate true "request"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/rsync/modules [get]
|
||||
func (r *RsyncController) List(ctx http.Context) http.Response {
|
||||
config, err := io.Read("/etc/rsyncd.conf")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
var modules []types.RsyncModule
|
||||
lines := strings.Split(config, "\n")
|
||||
var currentModule *types.RsyncModule
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||
if currentModule != nil {
|
||||
modules = append(modules, *currentModule)
|
||||
}
|
||||
moduleName := line[1 : len(line)-1]
|
||||
currentModule = &types.RsyncModule{
|
||||
Name: moduleName,
|
||||
}
|
||||
} else if currentModule != nil {
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
|
||||
switch key {
|
||||
case "path":
|
||||
currentModule.Path = value
|
||||
case "comment":
|
||||
currentModule.Comment = value
|
||||
case "read only":
|
||||
currentModule.ReadOnly = value == "yes" || value == "true"
|
||||
case "auth users":
|
||||
currentModule.AuthUser = value
|
||||
currentModule.Secret, err = shell.Execf("grep -E '^" + currentModule.AuthUser + ":.*$' /etc/rsyncd.secrets | awk -F ':' '{print $2}'")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取模块"+currentModule.AuthUser+"的密钥失败")
|
||||
}
|
||||
case "hosts allow":
|
||||
currentModule.HostsAllow = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if currentModule != nil {
|
||||
modules = append(modules, *currentModule)
|
||||
}
|
||||
|
||||
paged, total := controllers.Paginate(ctx, modules)
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
}
|
||||
|
||||
// Create
|
||||
//
|
||||
// @Summary 添加模块
|
||||
// @Description 添加 Rsync 模块
|
||||
// @Tags 插件-Rsync
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.Create true "request"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/rsync/modules [post]
|
||||
func (r *RsyncController) Create(ctx http.Context) http.Response {
|
||||
var createRequest requests.Create
|
||||
sanitize := controllers.SanitizeRequest(ctx, &createRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
config, err := io.Read("/etc/rsyncd.conf")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if strings.Contains(config, "["+createRequest.Name+"]") {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "模块 "+createRequest.Name+" 已存在")
|
||||
}
|
||||
|
||||
conf := `# ` + createRequest.Name + `-START
|
||||
[` + createRequest.Name + `]
|
||||
path = ` + createRequest.Path + `
|
||||
comment = ` + createRequest.Comment + `
|
||||
read only = no
|
||||
auth users = ` + createRequest.AuthUser + `
|
||||
hosts allow = ` + createRequest.HostsAllow + `
|
||||
secrets file = /etc/rsyncd.secrets
|
||||
# ` + createRequest.Name + `-END
|
||||
`
|
||||
|
||||
if err := io.WriteAppend("/etc/rsyncd.conf", conf); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if out, err := shell.Execf("echo '" + createRequest.AuthUser + ":" + createRequest.Secret + "' >> /etc/rsyncd.secrets"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
if err := systemctl.Restart("rsyncd"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Destroy
|
||||
//
|
||||
// @Summary 删除模块
|
||||
// @Description 删除 Rsync 模块
|
||||
// @Tags 插件-Rsync
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param name path string true "模块名称"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/rsync/modules/{name} [delete]
|
||||
func (r *RsyncController) Destroy(ctx http.Context) http.Response {
|
||||
name := ctx.Request().Input("name")
|
||||
if len(name) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "name 不能为空")
|
||||
}
|
||||
|
||||
config, err := io.Read("/etc/rsyncd.conf")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if !strings.Contains(config, "["+name+"]") {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "模块 "+name+" 不存在")
|
||||
}
|
||||
|
||||
module := str.Cut(config, "# "+name+"-START", "# "+name+"-END")
|
||||
config = strings.Replace(config, "\n# "+name+"-START"+module+"# "+name+"-END", "", -1)
|
||||
|
||||
match := regexp.MustCompile(`auth users = ([^\n]+)`).FindStringSubmatch(module)
|
||||
if len(match) == 2 {
|
||||
authUser := match[1]
|
||||
if out, err := shell.Execf("sed -i '/^" + authUser + ":.*$/d' /etc/rsyncd.secrets"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
}
|
||||
|
||||
if err = io.Write("/etc/rsyncd.conf", config, 0644); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("rsyncd"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Update
|
||||
//
|
||||
// @Summary 更新模块
|
||||
// @Description 更新 Rsync 模块
|
||||
// @Tags 插件-Rsync
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param name path string true "模块名称"
|
||||
// @Param data body requests.Update true "request"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/rsync/modules/{name} [post]
|
||||
func (r *RsyncController) Update(ctx http.Context) http.Response {
|
||||
var updateRequest requests.Update
|
||||
sanitize := controllers.SanitizeRequest(ctx, &updateRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
config, err := io.Read("/etc/rsyncd.conf")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if !strings.Contains(config, "["+updateRequest.Name+"]") {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "模块 "+updateRequest.Name+" 不存在")
|
||||
}
|
||||
|
||||
newConf := `# ` + updateRequest.Name + `-START
|
||||
[` + updateRequest.Name + `]
|
||||
path = ` + updateRequest.Path + `
|
||||
comment = ` + updateRequest.Comment + `
|
||||
read only = no
|
||||
auth users = ` + updateRequest.AuthUser + `
|
||||
hosts allow = ` + updateRequest.HostsAllow + `
|
||||
secrets file = /etc/rsyncd.secrets
|
||||
# ` + updateRequest.Name + `-END`
|
||||
|
||||
module := str.Cut(config, "# "+updateRequest.Name+"-START", "# "+updateRequest.Name+"-END")
|
||||
config = strings.Replace(config, "# "+updateRequest.Name+"-START"+module+"# "+updateRequest.Name+"-END", newConf, -1)
|
||||
|
||||
match := regexp.MustCompile(`auth users = ([^\n]+)`).FindStringSubmatch(module)
|
||||
if len(match) == 2 {
|
||||
authUser := match[1]
|
||||
if out, err := shell.Execf("sed -i '/^" + authUser + ":.*$/d' /etc/rsyncd.secrets"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
}
|
||||
|
||||
if err = io.Write("/etc/rsyncd.conf", config, 0644); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if out, err := shell.Execf("echo '" + updateRequest.AuthUser + ":" + updateRequest.Secret + "' >> /etc/rsyncd.secrets"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("rsyncd"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetConfig
|
||||
//
|
||||
// @Summary 获取配置
|
||||
// @Description 获取 Rsync 配置
|
||||
// @Tags 插件-Rsync
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/rsync/config [get]
|
||||
func (r *RsyncController) GetConfig(ctx http.Context) http.Response {
|
||||
config, err := io.Read("/etc/rsyncd.conf")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// UpdateConfig
|
||||
//
|
||||
// @Summary 更新配置
|
||||
// @Description 更新 Rsync 配置
|
||||
// @Tags 插件-Rsync
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.UpdateConfig true "request"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/rsync/config [post]
|
||||
func (r *RsyncController) UpdateConfig(ctx http.Context) http.Response {
|
||||
var updateRequest requests.UpdateConfig
|
||||
sanitize := controllers.SanitizeRequest(ctx, &updateRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
if err := io.Write("/etc/rsyncd.conf", updateRequest.Config, 0644); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if err := systemctl.Restart("rsyncd"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
"github.com/goravel/framework/support/json"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/TheTNB/panel/app/http/controllers"
|
||||
"github.com/TheTNB/panel/internal"
|
||||
"github.com/TheTNB/panel/internal/services"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type S3fsController struct {
|
||||
setting internal.Setting
|
||||
}
|
||||
|
||||
func NewS3fsController() *S3fsController {
|
||||
return &S3fsController{
|
||||
setting: services.NewSettingImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// List 所有 S3fs 挂载
|
||||
func (r *S3fsController) List(ctx http.Context) http.Response {
|
||||
var s3fsList []types.S3fsMount
|
||||
err := json.UnmarshalString(r.setting.Get("s3fs", "[]"), &s3fsList)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取 S3fs 挂载失败")
|
||||
}
|
||||
|
||||
paged, total := controllers.Paginate(ctx, s3fsList)
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
}
|
||||
|
||||
// Add 添加 S3fs 挂载
|
||||
func (r *S3fsController) Add(ctx http.Context) http.Response {
|
||||
if sanitize := controllers.Sanitize(ctx, map[string]string{
|
||||
"ak": "required|regex:^[a-zA-Z0-9]*$",
|
||||
"sk": "required|regex:^[a-zA-Z0-9]*$",
|
||||
"bucket": "required|regex:^[a-zA-Z0-9_-]*$",
|
||||
"url": "required|full_url",
|
||||
"path": "required|regex:^/[a-zA-Z0-9_-]+$",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
ak := ctx.Request().Input("ak")
|
||||
sk := ctx.Request().Input("sk")
|
||||
path := ctx.Request().Input("path")
|
||||
bucket := ctx.Request().Input("bucket")
|
||||
url := ctx.Request().Input("url")
|
||||
|
||||
// 检查下地域节点中是否包含bucket,如果包含了,肯定是错误的
|
||||
if strings.Contains(url, bucket) {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "地域节点不能包含 Bucket 名称")
|
||||
}
|
||||
|
||||
// 检查挂载目录是否存在且为空
|
||||
if !io.Exists(path) {
|
||||
if err := io.Mkdir(path, 0755); err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "挂载目录创建失败")
|
||||
}
|
||||
}
|
||||
if !io.Empty(path) {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "挂载目录必须为空")
|
||||
}
|
||||
|
||||
var s3fsList []types.S3fsMount
|
||||
if err := json.UnmarshalString(r.setting.Get("s3fs", "[]"), &s3fsList); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取 S3fs 挂载失败")
|
||||
}
|
||||
|
||||
for _, s := range s3fsList {
|
||||
if s.Path == path {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "路径已存在")
|
||||
}
|
||||
}
|
||||
|
||||
id := carbon.Now().TimestampMilli()
|
||||
password := ak + ":" + sk
|
||||
if err := io.Write("/etc/passwd-s3fs-"+cast.ToString(id), password, 0600); err != nil {
|
||||
return nil
|
||||
}
|
||||
out, err := shell.Execf(`echo 's3fs#` + bucket + ` ` + path + ` fuse _netdev,allow_other,nonempty,url=` + url + `,passwd_file=/etc/passwd-s3fs-` + cast.ToString(id) + ` 0 0' >> /etc/fstab`)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if mountCheck, err := shell.Execf("mount -a 2>&1"); err != nil {
|
||||
_, _ = shell.Execf(`sed -i 's@^s3fs#` + bucket + `\s` + path + `.*$@@g' /etc/fstab`)
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "检测到/etc/fstab有误: "+mountCheck)
|
||||
}
|
||||
if _, err := shell.Execf("df -h | grep " + path + " 2>&1"); err != nil {
|
||||
_, _ = shell.Execf(`sed -i 's@^s3fs#` + bucket + `\s` + path + `.*$@@g' /etc/fstab`)
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "挂载失败,请检查配置是否正确")
|
||||
}
|
||||
|
||||
s3fsList = append(s3fsList, types.S3fsMount{
|
||||
ID: id,
|
||||
Path: path,
|
||||
Bucket: bucket,
|
||||
Url: url,
|
||||
})
|
||||
encoded, err := json.MarshalString(s3fsList)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "添加 S3fs 挂载失败")
|
||||
}
|
||||
err = r.setting.Set("s3fs", encoded)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "添加 S3fs 挂载失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Delete 删除 S3fs 挂载
|
||||
func (r *S3fsController) Delete(ctx http.Context) http.Response {
|
||||
id := ctx.Request().Input("id")
|
||||
if len(id) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "挂载ID不能为空")
|
||||
}
|
||||
|
||||
var s3fsList []types.S3fsMount
|
||||
err := json.UnmarshalString(r.setting.Get("s3fs", "[]"), &s3fsList)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取 S3fs 挂载失败")
|
||||
}
|
||||
|
||||
var mount types.S3fsMount
|
||||
for _, s := range s3fsList {
|
||||
if cast.ToString(s.ID) == id {
|
||||
mount = s
|
||||
break
|
||||
}
|
||||
}
|
||||
if mount.ID == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "挂载ID不存在")
|
||||
}
|
||||
|
||||
if out, err := shell.Execf(`fusermount -u '` + mount.Path + `' 2>&1`); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if out, err := shell.Execf(`umount '` + mount.Path + `' 2>&1`); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if out, err := shell.Execf(`sed -i 's@^s3fs#` + mount.Bucket + `\s` + mount.Path + `.*$@@g' /etc/fstab`); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if mountCheck, err := shell.Execf("mount -a 2>&1"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "检测到/etc/fstab有误: "+mountCheck)
|
||||
}
|
||||
if err := io.Remove("/etc/passwd-s3fs-" + cast.ToString(mount.ID)); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
var newS3fsList []types.S3fsMount
|
||||
for _, s := range s3fsList {
|
||||
if s.ID != mount.ID {
|
||||
newS3fsList = append(newS3fsList, s)
|
||||
}
|
||||
}
|
||||
encoded, err := json.MarshalString(newS3fsList)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "删除 S3fs 挂载失败")
|
||||
}
|
||||
err = r.setting.Set("s3fs", encoded)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "删除 S3fs 挂载失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -1,336 +0,0 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
|
||||
"github.com/TheTNB/panel/app/http/controllers"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/os"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
)
|
||||
|
||||
type SupervisorController struct {
|
||||
service string
|
||||
}
|
||||
|
||||
func NewSupervisorController() *SupervisorController {
|
||||
var service string
|
||||
if os.IsRHEL() {
|
||||
service = "supervisord"
|
||||
} else {
|
||||
service = "supervisor"
|
||||
}
|
||||
|
||||
return &SupervisorController{
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
// Service 获取服务名称
|
||||
func (r *SupervisorController) Service(ctx http.Context) http.Response {
|
||||
return controllers.Success(ctx, r.service)
|
||||
}
|
||||
|
||||
// Log 日志
|
||||
func (r *SupervisorController) Log(ctx http.Context) http.Response {
|
||||
log, err := shell.Execf(`tail -n 200 /var/log/supervisor/supervisord.log`)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, log)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
// ClearLog 清空日志
|
||||
func (r *SupervisorController) ClearLog(ctx http.Context) http.Response {
|
||||
if out, err := shell.Execf(`echo "" > /var/log/supervisor/supervisord.log`); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Config 获取配置
|
||||
func (r *SupervisorController) Config(ctx http.Context) http.Response {
|
||||
var config string
|
||||
var err error
|
||||
if os.IsRHEL() {
|
||||
config, err = io.Read(`/etc/supervisord.conf`)
|
||||
} else {
|
||||
config, err = io.Read(`/etc/supervisor/supervisord.conf`)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveConfig 保存配置
|
||||
func (r *SupervisorController) SaveConfig(ctx http.Context) http.Response {
|
||||
config := ctx.Request().Input("config")
|
||||
var err error
|
||||
if os.IsRHEL() {
|
||||
err = io.Write(`/etc/supervisord.conf`, config, 0644)
|
||||
} else {
|
||||
err = io.Write(`/etc/supervisor/supervisord.conf`, config, 0644)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if err = systemctl.Restart(r.service); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重启 %s 服务失败", r.service))
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Processes 进程列表
|
||||
func (r *SupervisorController) Processes(ctx http.Context) http.Response {
|
||||
type process struct {
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
Pid string `json:"pid"`
|
||||
Uptime string `json:"uptime"`
|
||||
}
|
||||
|
||||
out, err := shell.Execf(`supervisorctl status | awk '{print $1}'`)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
var processes []process
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var p process
|
||||
p.Name = line
|
||||
if status, err := shell.Execf(`supervisorctl status ` + line + ` | awk '{print $2}'`); err == nil {
|
||||
p.Status = status
|
||||
}
|
||||
if p.Status == "RUNNING" {
|
||||
pid, _ := shell.Execf(`supervisorctl status ` + line + ` | awk '{print $4}'`)
|
||||
p.Pid = strings.ReplaceAll(pid, ",", "")
|
||||
uptime, _ := shell.Execf(`supervisorctl status ` + line + ` | awk '{print $6}'`)
|
||||
p.Uptime = uptime
|
||||
} else {
|
||||
p.Pid = "-"
|
||||
p.Uptime = "-"
|
||||
}
|
||||
processes = append(processes, p)
|
||||
}
|
||||
|
||||
paged, total := controllers.Paginate(ctx, processes)
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
}
|
||||
|
||||
// StartProcess 启动进程
|
||||
func (r *SupervisorController) StartProcess(ctx http.Context) http.Response {
|
||||
process := ctx.Request().Input("process")
|
||||
if out, err := shell.Execf(`supervisorctl start %s`, process); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// StopProcess 停止进程
|
||||
func (r *SupervisorController) StopProcess(ctx http.Context) http.Response {
|
||||
process := ctx.Request().Input("process")
|
||||
if out, err := shell.Execf(`supervisorctl stop %s`, process); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// RestartProcess 重启进程
|
||||
func (r *SupervisorController) RestartProcess(ctx http.Context) http.Response {
|
||||
process := ctx.Request().Input("process")
|
||||
if out, err := shell.Execf(`supervisorctl restart %s`, process); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// ProcessLog 进程日志
|
||||
func (r *SupervisorController) ProcessLog(ctx http.Context) http.Response {
|
||||
process := ctx.Request().Input("process")
|
||||
var logPath string
|
||||
var err error
|
||||
if os.IsRHEL() {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, process)
|
||||
} else {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, process)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "无法从进程"+process+"的配置文件中获取日志路径")
|
||||
}
|
||||
|
||||
log, err := shell.Execf(`tail -n 200 ` + logPath)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, log)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
// ClearProcessLog 清空进程日志
|
||||
func (r *SupervisorController) ClearProcessLog(ctx http.Context) http.Response {
|
||||
process := ctx.Request().Input("process")
|
||||
var logPath string
|
||||
var err error
|
||||
if os.IsRHEL() {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, process)
|
||||
} else {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, process)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("无法从进程%s的配置文件中获取日志路径", process))
|
||||
}
|
||||
|
||||
if out, err := shell.Execf(`echo "" > ` + logPath); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// ProcessConfig 获取进程配置
|
||||
func (r *SupervisorController) ProcessConfig(ctx http.Context) http.Response {
|
||||
process := ctx.Request().Query("process")
|
||||
var config string
|
||||
var err error
|
||||
if os.IsRHEL() {
|
||||
config, err = io.Read(`/etc/supervisord.d/` + process + `.conf`)
|
||||
} else {
|
||||
config, err = io.Read(`/etc/supervisor/conf.d/` + process + `.conf`)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveProcessConfig 保存进程配置
|
||||
func (r *SupervisorController) SaveProcessConfig(ctx http.Context) http.Response {
|
||||
process := ctx.Request().Input("process")
|
||||
config := ctx.Request().Input("config")
|
||||
var err error
|
||||
if os.IsRHEL() {
|
||||
err = io.Write(`/etc/supervisord.d/`+process+`.conf`, config, 0644)
|
||||
} else {
|
||||
err = io.Write(`/etc/supervisor/conf.d/`+process+`.conf`, config, 0644)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
|
||||
}
|
||||
|
||||
_, _ = shell.Execf(`supervisorctl reread`)
|
||||
_, _ = shell.Execf(`supervisorctl update`)
|
||||
_, _ = shell.Execf(`supervisorctl restart %s`, process)
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// AddProcess 添加进程
|
||||
func (r *SupervisorController) AddProcess(ctx http.Context) http.Response {
|
||||
if sanitize := controllers.Sanitize(ctx, map[string]string{
|
||||
"name": "required|alpha_dash",
|
||||
"user": "required|alpha_dash",
|
||||
"path": "required",
|
||||
"command": "required",
|
||||
"num": "required",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
name := ctx.Request().Input("name")
|
||||
user := ctx.Request().Input("user")
|
||||
path := ctx.Request().Input("path")
|
||||
command := ctx.Request().Input("command")
|
||||
num := ctx.Request().InputInt("num", 1)
|
||||
config := `[program:` + name + `]
|
||||
command=` + command + `
|
||||
process_name=%(program_name)s
|
||||
directory=` + path + `
|
||||
autostart=true
|
||||
autorestart=true
|
||||
user=` + user + `
|
||||
numprocs=` + strconv.Itoa(num) + `
|
||||
redirect_stderr=true
|
||||
stdout_logfile=/var/log/supervisor/` + name + `.log
|
||||
stdout_logfile_maxbytes=2MB
|
||||
`
|
||||
|
||||
var err error
|
||||
if os.IsRHEL() {
|
||||
err = io.Write(`/etc/supervisord.d/`+name+`.conf`, config, 0644)
|
||||
} else {
|
||||
err = io.Write(`/etc/supervisor/conf.d/`+name+`.conf`, config, 0644)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
|
||||
}
|
||||
|
||||
_, _ = shell.Execf(`supervisorctl reread`)
|
||||
_, _ = shell.Execf(`supervisorctl update`)
|
||||
_, _ = shell.Execf(`supervisorctl start %s`, name)
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// DeleteProcess 删除进程
|
||||
func (r *SupervisorController) DeleteProcess(ctx http.Context) http.Response {
|
||||
process := ctx.Request().Input("process")
|
||||
if out, err := shell.Execf(`supervisorctl stop %s`, process); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
var logPath string
|
||||
var err error
|
||||
if os.IsRHEL() {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, process)
|
||||
if err := io.Remove(`/etc/supervisord.d/` + process + `.conf`); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
} else {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, process)
|
||||
if err := io.Remove(`/etc/supervisor/conf.d/` + process + `.conf`); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "无法从进程"+process+"的配置文件中获取日志路径")
|
||||
}
|
||||
|
||||
if err := io.Remove(logPath); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
_, _ = shell.Execf(`supervisorctl reread`)
|
||||
_, _ = shell.Execf(`supervisorctl update`)
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -1,241 +0,0 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/TheTNB/panel/app/http/controllers"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
)
|
||||
|
||||
type ToolBoxController struct {
|
||||
}
|
||||
|
||||
func NewToolBoxController() *ToolBoxController {
|
||||
return &ToolBoxController{}
|
||||
}
|
||||
|
||||
// GetDNS 获取 DNS 信息
|
||||
func (r *ToolBoxController) GetDNS(ctx http.Context) http.Response {
|
||||
raw, err := io.Read("/etc/resolv.conf")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
match := regexp.MustCompile(`nameserver\s+(\S+)`).FindAllStringSubmatch(raw, -1)
|
||||
if len(match) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "找不到 DNS 信息")
|
||||
}
|
||||
|
||||
var dns []string
|
||||
for _, m := range match {
|
||||
dns = append(dns, m[1])
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, dns)
|
||||
}
|
||||
|
||||
// SetDNS 设置 DNS 信息
|
||||
func (r *ToolBoxController) SetDNS(ctx http.Context) http.Response {
|
||||
dns1 := ctx.Request().Input("dns1")
|
||||
dns2 := ctx.Request().Input("dns2")
|
||||
|
||||
if len(dns1) == 0 || len(dns2) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "DNS 信息不能为空")
|
||||
}
|
||||
|
||||
var dns string
|
||||
dns += "nameserver " + dns1 + "\n"
|
||||
dns += "nameserver " + dns2 + "\n"
|
||||
|
||||
if err := io.Write("/etc/resolv.conf", dns, 0644); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "写入 DNS 信息失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetSWAP 获取 SWAP 信息
|
||||
func (r *ToolBoxController) GetSWAP(ctx http.Context) http.Response {
|
||||
var total, used, free string
|
||||
var size int64
|
||||
if io.Exists("/www/swap") {
|
||||
file, err := io.FileInfo("/www/swap")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "获取 SWAP 信息失败")
|
||||
}
|
||||
|
||||
size = file.Size() / 1024 / 1024
|
||||
total = str.FormatBytes(float64(file.Size()))
|
||||
} else {
|
||||
size = 0
|
||||
total = "0.00 B"
|
||||
}
|
||||
|
||||
raw, err := shell.Execf("free | grep Swap")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "获取 SWAP 信息失败")
|
||||
}
|
||||
|
||||
match := regexp.MustCompile(`Swap:\s+(\d+)\s+(\d+)\s+(\d+)`).FindStringSubmatch(raw)
|
||||
if len(match) > 0 {
|
||||
used = str.FormatBytes(cast.ToFloat64(match[2]) * 1024)
|
||||
free = str.FormatBytes(cast.ToFloat64(match[3]) * 1024)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"size": size,
|
||||
"used": used,
|
||||
"free": free,
|
||||
})
|
||||
}
|
||||
|
||||
// SetSWAP 设置 SWAP 信息
|
||||
func (r *ToolBoxController) SetSWAP(ctx http.Context) http.Response {
|
||||
size := ctx.Request().InputInt("size")
|
||||
|
||||
if io.Exists("/www/swap") {
|
||||
if out, err := shell.Execf("swapoff /www/swap"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, out)
|
||||
}
|
||||
if out, err := shell.Execf("rm -f /www/swap"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, out)
|
||||
}
|
||||
if out, err := shell.Execf("sed -i '/www\\/swap/d' /etc/fstab"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, out)
|
||||
}
|
||||
}
|
||||
|
||||
if size > 1 {
|
||||
free, err := shell.Execf("df -k /www | awk '{print $4}' | tail -n 1")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "获取磁盘空间失败")
|
||||
}
|
||||
if cast.ToInt64(free)*1024 < int64(size)*1024*1024 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "磁盘空间不足,当前剩余 "+str.FormatBytes(cast.ToFloat64(free)))
|
||||
}
|
||||
|
||||
btrfsCheck, _ := shell.Execf("df -T /www | awk '{print $2}' | tail -n 1")
|
||||
if strings.Contains(btrfsCheck, "btrfs") {
|
||||
if out, err := shell.Execf("btrfs filesystem mkswapfile --size " + cast.ToString(size) + "M --uuid clear /www/swap"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, out)
|
||||
}
|
||||
} else {
|
||||
if out, err := shell.Execf("dd if=/dev/zero of=/www/swap bs=1M count=" + cast.ToString(size)); err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, out)
|
||||
}
|
||||
if out, err := shell.Execf("mkswap -f /www/swap"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, out)
|
||||
}
|
||||
if err := io.Chmod("/www/swap", 0600); err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "设置 SWAP 权限失败")
|
||||
}
|
||||
}
|
||||
if out, err := shell.Execf("swapon /www/swap"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, out)
|
||||
}
|
||||
if out, err := shell.Execf("echo '/www/swap swap swap defaults 0 0' >> /etc/fstab"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, out)
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetTimezone 获取时区
|
||||
func (r *ToolBoxController) GetTimezone(ctx http.Context) http.Response {
|
||||
raw, err := shell.Execf("timedatectl | grep zone")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "获取时区信息失败")
|
||||
}
|
||||
|
||||
match := regexp.MustCompile(`zone:\s+(\S+)`).FindStringSubmatch(raw)
|
||||
if len(match) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "找不到时区信息")
|
||||
}
|
||||
|
||||
type zone struct {
|
||||
Label string `json:"label"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
zonesRaw, err := shell.Execf("timedatectl list-timezones")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "获取时区列表失败")
|
||||
}
|
||||
zones := strings.Split(zonesRaw, "\n")
|
||||
|
||||
var zonesList []zone
|
||||
for _, z := range zones {
|
||||
zonesList = append(zonesList, zone{
|
||||
Label: z,
|
||||
Value: z,
|
||||
})
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"timezone": match[1],
|
||||
"timezones": zonesList,
|
||||
})
|
||||
}
|
||||
|
||||
// SetTimezone 设置时区
|
||||
func (r *ToolBoxController) SetTimezone(ctx http.Context) http.Response {
|
||||
timezone := ctx.Request().Input("timezone")
|
||||
if len(timezone) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "时区不能为空")
|
||||
}
|
||||
|
||||
if out, err := shell.Execf("timedatectl set-timezone %s", timezone); err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, out)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetHosts 获取 hosts 信息
|
||||
func (r *ToolBoxController) GetHosts(ctx http.Context) http.Response {
|
||||
hosts, err := io.Read("/etc/hosts")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, hosts)
|
||||
}
|
||||
|
||||
// SetHosts 设置 hosts 信息
|
||||
func (r *ToolBoxController) SetHosts(ctx http.Context) http.Response {
|
||||
hosts := ctx.Request().Input("hosts")
|
||||
if len(hosts) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "hosts 信息不能为空")
|
||||
}
|
||||
|
||||
if err := io.Write("/etc/hosts", hosts, 0644); err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "写入 hosts 信息失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// SetRootPassword 设置 root 密码
|
||||
func (r *ToolBoxController) SetRootPassword(ctx http.Context) http.Response {
|
||||
password := ctx.Request().Input("password")
|
||||
if len(password) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "密码不能为空")
|
||||
}
|
||||
if !regexp.MustCompile(`^[a-zA-Z0-9·~!@#$%^&*()_+-=\[\]{};:'",./<>?]{6,20}$`).MatchString(password) {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "密码必须为 6-20 位字母、数字或特殊字符")
|
||||
}
|
||||
|
||||
password = strings.ReplaceAll(password, `'`, `\'`)
|
||||
if out, err := shell.Execf(`yes '` + password + `' | passwd root`); err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, out)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -1,357 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/os"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
)
|
||||
|
||||
type SafeController struct {
|
||||
ssh string
|
||||
}
|
||||
|
||||
func NewSafeController() *SafeController {
|
||||
var ssh string
|
||||
if os.IsRHEL() {
|
||||
ssh = "sshd"
|
||||
} else {
|
||||
ssh = "ssh"
|
||||
}
|
||||
|
||||
return &SafeController{
|
||||
ssh: ssh,
|
||||
}
|
||||
}
|
||||
|
||||
// GetFirewallStatus 获取防火墙状态
|
||||
func (r *SafeController) GetFirewallStatus(ctx http.Context) http.Response {
|
||||
return Success(ctx, r.firewallStatus())
|
||||
}
|
||||
|
||||
// SetFirewallStatus 设置防火墙状态
|
||||
func (r *SafeController) SetFirewallStatus(ctx http.Context) http.Response {
|
||||
var err error
|
||||
if ctx.Request().InputBool("status") {
|
||||
if os.IsRHEL() {
|
||||
err = systemctl.Start("firewalld")
|
||||
if err == nil {
|
||||
err = systemctl.Enable("firewalld")
|
||||
}
|
||||
} else {
|
||||
_, err = shell.Execf("echo y | ufw enable")
|
||||
if err == nil {
|
||||
err = systemctl.Start("ufw")
|
||||
}
|
||||
if err == nil {
|
||||
err = systemctl.Enable("ufw")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if os.IsRHEL() {
|
||||
err = systemctl.Stop("firewalld")
|
||||
if err == nil {
|
||||
err = systemctl.Disable("firewalld")
|
||||
}
|
||||
} else {
|
||||
_, err = shell.Execf("ufw disable")
|
||||
if err == nil {
|
||||
err = systemctl.Stop("ufw")
|
||||
}
|
||||
if err == nil {
|
||||
err = systemctl.Disable("ufw")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetFirewallRules 获取防火墙规则
|
||||
func (r *SafeController) GetFirewallRules(ctx http.Context) http.Response {
|
||||
if !r.firewallStatus() {
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
var rules []map[string]string
|
||||
if os.IsRHEL() {
|
||||
out, err := shell.Execf("firewall-cmd --list-all")
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
match := regexp.MustCompile(`ports: (.*)`).FindStringSubmatch(out)
|
||||
if len(match) == 0 {
|
||||
return Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []map[string]string{},
|
||||
})
|
||||
}
|
||||
ports := strings.Split(match[1], " ")
|
||||
for _, port := range ports {
|
||||
rule := strings.Split(port, "/")
|
||||
rules = append(rules, map[string]string{
|
||||
"port": rule[0],
|
||||
"protocol": rule[1],
|
||||
})
|
||||
}
|
||||
} else {
|
||||
out, err := shell.Execf("ufw status | grep -v '(v6)' | grep ALLOW | awk '{print $1}'")
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
if len(out) == 0 {
|
||||
return Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []map[string]string{},
|
||||
})
|
||||
}
|
||||
for _, port := range strings.Split(out, "\n") {
|
||||
rule := strings.Split(port, "/")
|
||||
rules = append(rules, map[string]string{
|
||||
"port": rule[0],
|
||||
"protocol": rule[1],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
paged, total := Paginate(ctx, rules)
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
}
|
||||
|
||||
// AddFirewallRule 添加防火墙规则
|
||||
func (r *SafeController) AddFirewallRule(ctx http.Context) http.Response {
|
||||
if !r.firewallStatus() {
|
||||
return Error(ctx, http.StatusInternalServerError, "防火墙未启动")
|
||||
}
|
||||
|
||||
port := ctx.Request().Input("port")
|
||||
protocol := ctx.Request().Input("protocol")
|
||||
if port == "" || protocol == "" || (protocol != "tcp" && protocol != "udp") {
|
||||
return Error(ctx, http.StatusUnprocessableEntity, "参数错误")
|
||||
}
|
||||
// 端口有 2 种写法,一种是 80-443,一种是 80
|
||||
if strings.Contains(port, "-") {
|
||||
ports := strings.Split(port, "-")
|
||||
startPort := cast.ToInt(ports[0])
|
||||
endPort := cast.ToInt(ports[1])
|
||||
if startPort < 1 || startPort > 65535 || endPort < 1 || endPort > 65535 || startPort > endPort {
|
||||
return Error(ctx, http.StatusUnprocessableEntity, "参数错误")
|
||||
}
|
||||
} else {
|
||||
port := cast.ToInt(port)
|
||||
if port < 1 || port > 65535 {
|
||||
return Error(ctx, http.StatusUnprocessableEntity, "参数错误")
|
||||
}
|
||||
}
|
||||
|
||||
if os.IsRHEL() {
|
||||
if out, err := shell.Execf("firewall-cmd --remove-port=%s/%s --permanent", port, protocol); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if out, err := shell.Execf("firewall-cmd --add-port=%s/%s --permanent", port, protocol); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if out, err := shell.Execf("firewall-cmd --reload"); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
} else {
|
||||
// ufw 需要替换 - 为 : 添加
|
||||
if strings.Contains(port, "-") {
|
||||
port = strings.ReplaceAll(port, "-", ":")
|
||||
}
|
||||
if out, err := shell.Execf("ufw delete allow %s/%s", port, protocol); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if out, err := shell.Execf("ufw allow %s/%s", port, protocol); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if out, err := shell.Execf("ufw reload"); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// DeleteFirewallRule 删除防火墙规则
|
||||
func (r *SafeController) DeleteFirewallRule(ctx http.Context) http.Response {
|
||||
if !r.firewallStatus() {
|
||||
return Error(ctx, http.StatusUnprocessableEntity, "防火墙未启动")
|
||||
}
|
||||
|
||||
port := ctx.Request().Input("port")
|
||||
protocol := ctx.Request().Input("protocol")
|
||||
if port == "" || protocol == "" {
|
||||
return Error(ctx, http.StatusUnprocessableEntity, "参数错误")
|
||||
}
|
||||
|
||||
if os.IsRHEL() {
|
||||
if out, err := shell.Execf("firewall-cmd --remove-port=%s/%s --permanent", port, protocol); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if out, err := shell.Execf("firewall-cmd --reload"); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
} else {
|
||||
if out, err := shell.Execf("ufw delete allow %s/%s", port, protocol); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if out, err := shell.Execf("ufw reload"); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// firewallStatus 获取防火墙状态
|
||||
func (r *SafeController) firewallStatus() bool {
|
||||
var running bool
|
||||
if os.IsRHEL() {
|
||||
running, _ = systemctl.Status("firewalld")
|
||||
} else {
|
||||
running, _ = systemctl.Status("ufw")
|
||||
}
|
||||
|
||||
return running
|
||||
}
|
||||
|
||||
// GetSshStatus 获取 SSH 状态
|
||||
func (r *SafeController) GetSshStatus(ctx http.Context) http.Response {
|
||||
running, err := systemctl.Status(r.ssh)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, running)
|
||||
}
|
||||
|
||||
// SetSshStatus 设置 SSH 状态
|
||||
func (r *SafeController) SetSshStatus(ctx http.Context) http.Response {
|
||||
if ctx.Request().InputBool("status") {
|
||||
if err := systemctl.Enable(r.ssh); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if err := systemctl.Start(r.ssh); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
} else {
|
||||
if err := systemctl.Stop(r.ssh); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if err := systemctl.Disable(r.ssh); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetSshPort 获取 SSH 端口
|
||||
func (r *SafeController) GetSshPort(ctx http.Context) http.Response {
|
||||
out, err := shell.Execf("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'")
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
return Success(ctx, cast.ToInt(out))
|
||||
}
|
||||
|
||||
// SetSshPort 设置 SSH 端口
|
||||
func (r *SafeController) SetSshPort(ctx http.Context) http.Response {
|
||||
port := ctx.Request().InputInt("port", 0)
|
||||
if port == 0 {
|
||||
return Error(ctx, http.StatusUnprocessableEntity, "参数错误")
|
||||
}
|
||||
|
||||
oldPort, err := shell.Execf("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'")
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, oldPort)
|
||||
}
|
||||
_, _ = shell.Execf("sed -i 's/#Port %s/Port %d/g' /etc/ssh/sshd_config", oldPort, port)
|
||||
_, _ = shell.Execf("sed -i 's/Port %s/Port %d/g' /etc/ssh/sshd_config", oldPort, port)
|
||||
|
||||
status, _ := systemctl.Status(r.ssh)
|
||||
if status {
|
||||
_ = systemctl.Restart(r.ssh)
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetPingStatus 获取 Ping 状态
|
||||
func (r *SafeController) GetPingStatus(ctx http.Context) http.Response {
|
||||
if os.IsRHEL() {
|
||||
out, err := shell.Execf(`firewall-cmd --list-all`)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
if !strings.Contains(out, `rule protocol value="icmp" drop`) {
|
||||
return Success(ctx, true)
|
||||
} else {
|
||||
return Success(ctx, false)
|
||||
}
|
||||
} else {
|
||||
config, err := io.Read("/etc/ufw/before.rules")
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if strings.Contains(config, "-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT") {
|
||||
return Success(ctx, true)
|
||||
} else {
|
||||
return Success(ctx, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetPingStatus 设置 Ping 状态
|
||||
func (r *SafeController) SetPingStatus(ctx http.Context) http.Response {
|
||||
var out string
|
||||
var err error
|
||||
if os.IsRHEL() {
|
||||
if ctx.Request().InputBool("status") {
|
||||
out, err = shell.Execf(`firewall-cmd --permanent --remove-rich-rule='rule protocol value=icmp drop'`)
|
||||
} else {
|
||||
out, err = shell.Execf(`firewall-cmd --permanent --add-rich-rule='rule protocol value=icmp drop'`)
|
||||
}
|
||||
} else {
|
||||
if ctx.Request().InputBool("status") {
|
||||
out, err = shell.Execf(`sed -i 's/-A ufw-before-input -p icmp --icmp-type echo-request -j DROP/-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT/g' /etc/ufw/before.rules`)
|
||||
} else {
|
||||
out, err = shell.Execf(`sed -i 's/-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT/-A ufw-before-input -p icmp --icmp-type echo-request -j DROP/g' /etc/ufw/before.rules`)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
if os.IsRHEL() {
|
||||
out, err = shell.Execf(`firewall-cmd --reload`)
|
||||
} else {
|
||||
out, err = shell.Execf(`ufw reload`)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
@@ -1,290 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
requests "github.com/TheTNB/panel/app/http/requests/setting"
|
||||
"github.com/TheTNB/panel/app/models"
|
||||
"github.com/TheTNB/panel/internal"
|
||||
"github.com/TheTNB/panel/internal/services"
|
||||
"github.com/TheTNB/panel/pkg/cert"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/os"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/tools"
|
||||
)
|
||||
|
||||
type SettingController struct {
|
||||
setting internal.Setting
|
||||
}
|
||||
|
||||
func NewSettingController() *SettingController {
|
||||
return &SettingController{
|
||||
setting: services.NewSettingImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// List
|
||||
//
|
||||
// @Summary 设置列表
|
||||
// @Tags 面板设置
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/setting/list [get]
|
||||
func (r *SettingController) List(ctx http.Context) http.Response {
|
||||
var settings []models.Setting
|
||||
err := facades.Orm().Query().Get(&settings)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("获取面板设置列表失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
var user models.User
|
||||
err = facades.Auth(ctx).User(&user)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("获取用户信息失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
port, err := shell.Execf(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("获取面板端口失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"name": r.setting.Get(models.SettingKeyName),
|
||||
"language": facades.Config().GetString("app.locale"),
|
||||
"entrance": facades.Config().GetString("http.entrance"),
|
||||
"ssl": facades.Config().GetBool("panel.ssl"),
|
||||
"website_path": r.setting.Get(models.SettingKeyWebsitePath),
|
||||
"backup_path": r.setting.Get(models.SettingKeyBackupPath),
|
||||
"username": user.Username,
|
||||
"password": "",
|
||||
"email": user.Email,
|
||||
"port": port,
|
||||
})
|
||||
}
|
||||
|
||||
// Update
|
||||
//
|
||||
// @Summary 更新设置
|
||||
// @Tags 面板设置
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.Update true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/setting/update [post]
|
||||
func (r *SettingController) Update(ctx http.Context) http.Response {
|
||||
var updateRequest requests.Update
|
||||
sanitize := SanitizeRequest(ctx, &updateRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
err := r.setting.Set(models.SettingKeyName, updateRequest.Name)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("保存面板名称失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
if !io.Exists(updateRequest.BackupPath) {
|
||||
if err = io.Mkdir(updateRequest.BackupPath, 0644); err != nil {
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
}
|
||||
err = r.setting.Set(models.SettingKeyBackupPath, updateRequest.BackupPath)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("保存备份目录失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
if !io.Exists(updateRequest.WebsitePath) {
|
||||
if err = io.Mkdir(updateRequest.WebsitePath, 0755); err != nil {
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
if err = io.Chown(updateRequest.WebsitePath, "www", "www"); err != nil {
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
}
|
||||
err = r.setting.Set(models.SettingKeyWebsitePath, updateRequest.WebsitePath)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("保存建站目录失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
var user models.User
|
||||
err = facades.Auth(ctx).User(&user)
|
||||
if err != nil {
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
user.Username = updateRequest.UserName
|
||||
user.Email = updateRequest.Email
|
||||
if len(updateRequest.Password) > 0 {
|
||||
hash, err := facades.Hash().Make(updateRequest.Password)
|
||||
if err != nil {
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
user.Password = hash
|
||||
}
|
||||
if err = facades.Orm().Query().Save(&user); err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("保存用户信息失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
oldPort, err := shell.Execf(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("获取面板端口失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
port := cast.ToString(updateRequest.Port)
|
||||
if oldPort != port {
|
||||
if out, err := shell.Execf("sed -i 's/APP_PORT=%s/APP_PORT=%s/g' /www/panel/panel.conf", oldPort, port); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if os.IsRHEL() {
|
||||
if out, err := shell.Execf("firewall-cmd --remove-port=%s/tcp --permanent", oldPort); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if out, err := shell.Execf("firewall-cmd --add-port=%s/tcp --permanent", port); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if out, err := shell.Execf("firewall-cmd --reload"); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
} else {
|
||||
if out, err := shell.Execf("ufw delete allow %s/tcp", oldPort); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if out, err := shell.Execf("ufw allow %s/tcp", port); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if out, err := shell.Execf("ufw reload"); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
oldEntrance, err := shell.Execf(`cat /www/panel/panel.conf | grep APP_ENTRANCE | awk -F '=' '{print $2}' | tr -d '\n'`)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("获取面板入口失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
entrance := cast.ToString(updateRequest.Entrance)
|
||||
if oldEntrance != entrance {
|
||||
if out, err := shell.Execf("sed -i 's!APP_ENTRANCE=" + oldEntrance + "!APP_ENTRANCE=" + entrance + "!g' /www/panel/panel.conf"); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
}
|
||||
|
||||
oldLanguage, err := shell.Execf(`cat /www/panel/panel.conf | grep APP_LOCALE | awk -F '=' '{print $2}' | tr -d '\n'`)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("获取面板语言失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
if oldLanguage != updateRequest.Language {
|
||||
if out, err := shell.Execf("sed -i 's/APP_LOCALE=" + oldLanguage + "/APP_LOCALE=" + updateRequest.Language + "/g' /www/panel/panel.conf"); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
}
|
||||
|
||||
if oldPort != port || oldEntrance != entrance || oldLanguage != updateRequest.Language {
|
||||
tools.RestartPanel()
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetHttps
|
||||
//
|
||||
// @Summary 获取面板 HTTPS 设置
|
||||
// @Tags 面板设置
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/setting/https [get]
|
||||
func (r *SettingController) GetHttps(ctx http.Context) http.Response {
|
||||
certPath := facades.Config().GetString("http.tls.ssl.cert")
|
||||
keyPath := facades.Config().GetString("http.tls.ssl.key")
|
||||
crt, err := io.Read(certPath)
|
||||
if err != nil {
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
key, err := io.Read(keyPath)
|
||||
if err != nil {
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"https": facades.Config().GetBool("panel.ssl"),
|
||||
"cert": crt,
|
||||
"key": key,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateHttps
|
||||
//
|
||||
// @Summary 更新面板 HTTPS 设置
|
||||
// @Tags 面板设置
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.Https true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/setting/https [post]
|
||||
func (r *SettingController) UpdateHttps(ctx http.Context) http.Response {
|
||||
var httpsRequest requests.Https
|
||||
sanitize := SanitizeRequest(ctx, &httpsRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
if httpsRequest.Https {
|
||||
if _, err := cert.ParseCert(httpsRequest.Cert); err != nil {
|
||||
return Error(ctx, http.StatusBadRequest, "证书格式错误")
|
||||
}
|
||||
if _, err := cert.ParseKey(httpsRequest.Key); err != nil {
|
||||
return Error(ctx, http.StatusBadRequest, "密钥格式错误")
|
||||
}
|
||||
if err := io.Write(facades.App().ExecutablePath("storage/ssl.crt"), httpsRequest.Cert, 0700); err != nil {
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
if err := io.Write(facades.App().ExecutablePath("storage/ssl.key"), httpsRequest.Key, 0700); err != nil {
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
if out, err := shell.Execf("sed -i 's/APP_SSL=false/APP_SSL=true/g' /www/panel/panel.conf"); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
} else {
|
||||
if out, err := shell.Execf("sed -i 's/APP_SSL=true/APP_SSL=false/g' /www/panel/panel.conf"); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
}
|
||||
|
||||
tools.RestartPanel()
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
nethttp "net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/TheTNB/panel/app/models"
|
||||
"github.com/TheTNB/panel/internal"
|
||||
"github.com/TheTNB/panel/internal/services"
|
||||
"github.com/TheTNB/panel/pkg/ssh"
|
||||
)
|
||||
|
||||
type SshController struct {
|
||||
AuthMethod ssh.AuthMethod
|
||||
setting internal.Setting
|
||||
}
|
||||
|
||||
func NewSshController() *SshController {
|
||||
return &SshController{
|
||||
AuthMethod: ssh.PASSWORD,
|
||||
setting: services.NewSettingImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetInfo 获取 SSH 配置
|
||||
func (r *SshController) GetInfo(ctx http.Context) http.Response {
|
||||
host := r.setting.Get(models.SettingKeySshHost)
|
||||
port := r.setting.Get(models.SettingKeySshPort)
|
||||
user := r.setting.Get(models.SettingKeySshUser)
|
||||
password := r.setting.Get(models.SettingKeySshPassword)
|
||||
if len(host) == 0 || len(user) == 0 || len(password) == 0 {
|
||||
return Error(ctx, http.StatusInternalServerError, "SSH 配置不完整")
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"host": host,
|
||||
"port": cast.ToInt(port),
|
||||
"user": user,
|
||||
"password": password,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateInfo 更新 SSH 配置
|
||||
func (r *SshController) UpdateInfo(ctx http.Context) http.Response {
|
||||
if sanitize := Sanitize(ctx, map[string]string{
|
||||
"host": "required",
|
||||
"port": "required",
|
||||
"user": "required",
|
||||
"password": "required",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
host := ctx.Request().Input("host")
|
||||
port := ctx.Request().Input("port")
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
if err := r.setting.Set(models.SettingKeySshHost, host); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if err := r.setting.Set(models.SettingKeySshPort, port); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if err := r.setting.Set(models.SettingKeySshUser, user); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if err := r.setting.Set(models.SettingKeySshPassword, password); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Session SSH 会话
|
||||
func (r *SshController) Session(ctx http.Context) http.Response {
|
||||
upGrader := websocket.Upgrader{
|
||||
ReadBufferSize: 4096,
|
||||
WriteBufferSize: 4096,
|
||||
CheckOrigin: func(r *nethttp.Request) bool {
|
||||
return true
|
||||
},
|
||||
Subprotocols: []string{ctx.Request().Header("Sec-WebSocket-Protocol")},
|
||||
}
|
||||
|
||||
ws, err := upGrader.Upgrade(ctx.Response().Writer(), ctx.Request().Origin(), nil)
|
||||
if err != nil {
|
||||
facades.Log().Tags("面板", "SSH").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Infof("建立连接失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
config := ssh.ClientConfigPassword(
|
||||
r.setting.Get(models.SettingKeySshHost)+":"+r.setting.Get(models.SettingKeySshPort),
|
||||
r.setting.Get(models.SettingKeySshUser),
|
||||
r.setting.Get(models.SettingKeySshPassword),
|
||||
)
|
||||
client, err := ssh.NewSSHClient(config)
|
||||
|
||||
if err != nil {
|
||||
_ = ws.WriteControl(websocket.CloseMessage,
|
||||
[]byte(err.Error()), time.Now().Add(time.Second))
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
turn, err := ssh.NewTurn(ws, client)
|
||||
if err != nil {
|
||||
_ = ws.WriteControl(websocket.CloseMessage,
|
||||
[]byte(err.Error()), time.Now().Add(time.Second))
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
defer turn.Close()
|
||||
|
||||
var bufPool = sync.Pool{
|
||||
New: func() any {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
var logBuff = bufPool.Get().(*bytes.Buffer)
|
||||
logBuff.Reset()
|
||||
defer bufPool.Put(logBuff)
|
||||
|
||||
sshCtx, cancel := context.WithCancel(context.Background())
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err = turn.LoopRead(logBuff, sshCtx); err != nil {
|
||||
facades.Log().Tags("面板", "SSH").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Infof("读取数据失败")
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err = turn.SessionWait(); err != nil {
|
||||
facades.Log().Tags("面板", "SSH").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Infof("会话错误")
|
||||
}
|
||||
cancel()
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/swaggo/http-swagger/v2"
|
||||
|
||||
_ "github.com/TheTNB/panel/docs"
|
||||
)
|
||||
|
||||
type SwaggerController struct {
|
||||
// Dependent services
|
||||
}
|
||||
|
||||
func NewSwaggerController() *SwaggerController {
|
||||
return &SwaggerController{}
|
||||
}
|
||||
|
||||
// Index
|
||||
//
|
||||
// @Summary Swagger UI
|
||||
// @Description Swagger UI
|
||||
// @Tags Swagger
|
||||
// @Success 200
|
||||
// @Failure 500
|
||||
// @Router /swagger [get]
|
||||
func (r *SwaggerController) Index(ctx http.Context) http.Response {
|
||||
if !facades.Config().GetBool("app.debug") {
|
||||
return Error(ctx, http.StatusNotFound, http.StatusText(http.StatusNotFound))
|
||||
}
|
||||
|
||||
handler := httpSwagger.Handler()
|
||||
handler(ctx.Response().Writer(), ctx.Request().Origin())
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
)
|
||||
|
||||
type SystemController struct {
|
||||
}
|
||||
|
||||
func NewSystemController() *SystemController {
|
||||
return &SystemController{}
|
||||
}
|
||||
|
||||
// ServiceStatus
|
||||
//
|
||||
// @Summary 服务状态
|
||||
// @Tags 系统
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data query string true "request"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /panel/system/service/status [get]
|
||||
func (r *SystemController) ServiceStatus(ctx http.Context) http.Response {
|
||||
if sanitize := Sanitize(ctx, map[string]string{
|
||||
"service": "required|string",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
service := ctx.Request().Query("service")
|
||||
status, err := systemctl.Status(service)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, fmt.Sprintf("获取 %s 服务运行状态失败", service))
|
||||
}
|
||||
|
||||
return Success(ctx, status)
|
||||
}
|
||||
|
||||
// ServiceIsEnabled
|
||||
//
|
||||
// @Summary 是否启用服务
|
||||
// @Tags 系统
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data query string true "request"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /panel/system/service/isEnabled [get]
|
||||
func (r *SystemController) ServiceIsEnabled(ctx http.Context) http.Response {
|
||||
if sanitize := Sanitize(ctx, map[string]string{
|
||||
"service": "required|string",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
service := ctx.Request().Query("service")
|
||||
enabled, err := systemctl.IsEnabled(service)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, fmt.Sprintf("获取 %s 服务启用状态失败", service))
|
||||
}
|
||||
|
||||
return Success(ctx, enabled)
|
||||
}
|
||||
|
||||
// ServiceEnable
|
||||
//
|
||||
// @Summary 启用服务
|
||||
// @Tags 系统
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body string true "request"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /panel/system/service/enable [post]
|
||||
func (r *SystemController) ServiceEnable(ctx http.Context) http.Response {
|
||||
if sanitize := Sanitize(ctx, map[string]string{
|
||||
"service": "required|string",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
service := ctx.Request().Input("service")
|
||||
if err := systemctl.Enable(service); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, fmt.Sprintf("启用 %s 服务失败", service))
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// ServiceDisable
|
||||
//
|
||||
// @Summary 禁用服务
|
||||
// @Tags 系统
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body string true "request"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /panel/system/service/disable [post]
|
||||
func (r *SystemController) ServiceDisable(ctx http.Context) http.Response {
|
||||
if sanitize := Sanitize(ctx, map[string]string{
|
||||
"service": "required|string",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
service := ctx.Request().Input("service")
|
||||
if err := systemctl.Disable(service); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, fmt.Sprintf("禁用 %s 服务失败", service))
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// ServiceRestart
|
||||
//
|
||||
// @Summary 重启服务
|
||||
// @Tags 系统
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body string true "request"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /panel/system/service/restart [post]
|
||||
func (r *SystemController) ServiceRestart(ctx http.Context) http.Response {
|
||||
if sanitize := Sanitize(ctx, map[string]string{
|
||||
"service": "required|string",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
service := ctx.Request().Input("service")
|
||||
if err := systemctl.Restart(service); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重启 %s 服务失败", service))
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// ServiceReload
|
||||
//
|
||||
// @Summary 重载服务
|
||||
// @Tags 系统
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body string true "request"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /panel/system/service/reload [post]
|
||||
func (r *SystemController) ServiceReload(ctx http.Context) http.Response {
|
||||
if sanitize := Sanitize(ctx, map[string]string{
|
||||
"service": "required|string",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
service := ctx.Request().Input("service")
|
||||
if err := systemctl.Reload(service); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载 %s 服务失败", service))
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// ServiceStart
|
||||
//
|
||||
// @Summary 启动服务
|
||||
// @Tags 系统
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body string true "request"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /panel/system/service/start [post]
|
||||
func (r *SystemController) ServiceStart(ctx http.Context) http.Response {
|
||||
if sanitize := Sanitize(ctx, map[string]string{
|
||||
"service": "required|string",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
service := ctx.Request().Input("service")
|
||||
if err := systemctl.Start(service); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, fmt.Sprintf("启动 %s 服务失败", service))
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// ServiceStop
|
||||
//
|
||||
// @Summary 停止服务
|
||||
// @Tags 系统
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body string true "request"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /panel/system/service/stop [post]
|
||||
func (r *SystemController) ServiceStop(ctx http.Context) http.Response {
|
||||
if sanitize := Sanitize(ctx, map[string]string{
|
||||
"service": "required|string",
|
||||
}); sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
service := ctx.Request().Input("service")
|
||||
if err := systemctl.Stop(service); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, fmt.Sprintf("停止 %s 服务失败", service))
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"github.com/TheTNB/panel/app/models"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
)
|
||||
|
||||
type TaskController struct {
|
||||
// Dependent services
|
||||
}
|
||||
|
||||
func NewTaskController() *TaskController {
|
||||
return &TaskController{
|
||||
// Inject services
|
||||
}
|
||||
}
|
||||
|
||||
// Status 获取当前任务状态
|
||||
func (r *TaskController) Status(ctx http.Context) http.Response {
|
||||
var task models.Task
|
||||
err := facades.Orm().Query().Where("status", models.TaskStatusWaiting).OrWhere("status", models.TaskStatusRunning).FirstOrFail(&task)
|
||||
if err == nil {
|
||||
return Success(ctx, http.Json{
|
||||
"task": true,
|
||||
})
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"task": false,
|
||||
})
|
||||
}
|
||||
|
||||
// List 获取任务列表
|
||||
func (r *TaskController) List(ctx http.Context) http.Response {
|
||||
var tasks []models.Task
|
||||
var total int64
|
||||
err := facades.Orm().Query().Order("id desc").Paginate(ctx.Request().QueryInt("page", 1), ctx.Request().QueryInt("limit", 10), &tasks, &total)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "任务中心").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("查询任务列表失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"items": tasks,
|
||||
})
|
||||
}
|
||||
|
||||
// Log 获取任务日志
|
||||
func (r *TaskController) Log(ctx http.Context) http.Response {
|
||||
var task models.Task
|
||||
err := facades.Orm().Query().Where("id", ctx.Request().QueryInt("id")).FirstOrFail(&task)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "任务中心").With(map[string]any{
|
||||
"id": ctx.Request().QueryInt("id"),
|
||||
"error": err.Error(),
|
||||
}).Info("查询任务失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
log, err := shell.Execf(`tail -n 500 '` + task.Log + `'`)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, "日志已被清理")
|
||||
}
|
||||
|
||||
return Success(ctx, log)
|
||||
}
|
||||
|
||||
// Delete 删除任务
|
||||
func (r *TaskController) Delete(ctx http.Context) http.Response {
|
||||
var task models.Task
|
||||
_, err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).Delete(&task)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "任务中心").With(map[string]any{
|
||||
"id": ctx.Request().QueryInt("id"),
|
||||
"error": err.Error(),
|
||||
}).Info("删除任务失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"github.com/TheTNB/panel/app/http/requests/user"
|
||||
"github.com/TheTNB/panel/app/models"
|
||||
)
|
||||
|
||||
type UserController struct {
|
||||
// Dependent services
|
||||
}
|
||||
|
||||
func NewUserController() *UserController {
|
||||
return &UserController{
|
||||
// Inject services
|
||||
}
|
||||
}
|
||||
|
||||
// Login
|
||||
//
|
||||
// @Summary 登录
|
||||
// @Description 通过用户名和密码获取访问令牌
|
||||
// @Tags 用户鉴权
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body requests.Login true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Failure 403 {object} ErrorResponse "用户名或密码错误"
|
||||
// @Failure 500 {object} ErrorResponse "系统内部错误
|
||||
// @Router /panel/user/login [post]
|
||||
func (r *UserController) Login(ctx http.Context) http.Response {
|
||||
var loginRequest requests.Login
|
||||
sanitize := SanitizeRequest(ctx, &loginRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
var user models.User
|
||||
err := facades.Orm().Query().Where("username", loginRequest.Username).First(&user)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "用户").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("查询用户失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
if user.ID == 0 || !facades.Hash().Check(loginRequest.Password, user.Password) {
|
||||
return Error(ctx, http.StatusForbidden, "用户名或密码错误")
|
||||
}
|
||||
|
||||
if facades.Hash().NeedsRehash(user.Password) {
|
||||
user.Password, err = facades.Hash().Make(loginRequest.Password)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "用户").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("更新密码失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
token, loginErr := facades.Auth(ctx).LoginUsingID(user.ID)
|
||||
if loginErr != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "用户").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("登录失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"access_token": token,
|
||||
})
|
||||
}
|
||||
|
||||
// Info
|
||||
//
|
||||
// @Summary 用户信息
|
||||
// @Description 获取当前登录用户信息
|
||||
// @Tags 用户鉴权
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/user/info [get]
|
||||
func (r *UserController) Info(ctx http.Context) http.Response {
|
||||
var user models.User
|
||||
err := facades.Auth(ctx).User(&user)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "用户").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("获取用户信息失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"id": user.ID,
|
||||
"role": []string{"admin"},
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
})
|
||||
}
|
||||
@@ -1,662 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
commonrequests "github.com/TheTNB/panel/app/http/requests/common"
|
||||
requests "github.com/TheTNB/panel/app/http/requests/website"
|
||||
"github.com/TheTNB/panel/app/models"
|
||||
"github.com/TheTNB/panel/internal"
|
||||
"github.com/TheTNB/panel/internal/services"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type WebsiteController struct {
|
||||
website internal.Website
|
||||
setting internal.Setting
|
||||
backup internal.Backup
|
||||
}
|
||||
|
||||
func NewWebsiteController() *WebsiteController {
|
||||
return &WebsiteController{
|
||||
website: services.NewWebsiteImpl(),
|
||||
setting: services.NewSettingImpl(),
|
||||
backup: services.NewBackupImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// List
|
||||
//
|
||||
// @Summary 获取网站列表
|
||||
// @Tags 网站
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data query commonrequests.Paginate true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/websites [get]
|
||||
func (r *WebsiteController) List(ctx http.Context) http.Response {
|
||||
var paginateRequest commonrequests.Paginate
|
||||
sanitize := SanitizeRequest(ctx, &paginateRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
total, websites, err := r.website.List(paginateRequest.Page, paginateRequest.Limit)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("获取网站列表失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"items": websites,
|
||||
})
|
||||
}
|
||||
|
||||
// Add
|
||||
//
|
||||
// @Summary 添加网站
|
||||
// @Tags 网站
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.Add true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/websites [post]
|
||||
func (r *WebsiteController) Add(ctx http.Context) http.Response {
|
||||
var addRequest requests.Add
|
||||
sanitize := SanitizeRequest(ctx, &addRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
if len(addRequest.Path) == 0 {
|
||||
addRequest.Path = r.setting.Get(models.SettingKeyWebsitePath) + "/" + addRequest.Name
|
||||
}
|
||||
|
||||
website := types.WebsiteAdd{
|
||||
Name: addRequest.Name,
|
||||
Status: true,
|
||||
Domains: addRequest.Domains,
|
||||
Ports: addRequest.Ports,
|
||||
Path: addRequest.Path,
|
||||
Php: addRequest.Php,
|
||||
Ssl: false,
|
||||
Db: addRequest.Db,
|
||||
DbType: addRequest.DbType,
|
||||
DbName: addRequest.DbName,
|
||||
DbUser: addRequest.DbUser,
|
||||
DbPassword: addRequest.DbPassword,
|
||||
}
|
||||
|
||||
_, err := r.website.Add(website)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("添加网站失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Delete
|
||||
//
|
||||
// @Summary 删除网站
|
||||
// @Tags 网站
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param id path int true "网站 ID"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/websites/{id} [delete]
|
||||
func (r *WebsiteController) Delete(ctx http.Context) http.Response {
|
||||
var idRequest requests.ID
|
||||
sanitize := SanitizeRequest(ctx, &idRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
err := r.website.Delete(idRequest.ID)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{
|
||||
"id": idRequest.ID,
|
||||
"error": err.Error(),
|
||||
}).Info("删除网站失败")
|
||||
return Error(ctx, http.StatusInternalServerError, "删除网站失败: "+err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetDefaultConfig
|
||||
//
|
||||
// @Summary 获取默认配置
|
||||
// @Tags 网站
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Success 200 {object} SuccessResponse{data=map[string]string}
|
||||
// @Router /panel/website/defaultConfig [get]
|
||||
func (r *WebsiteController) GetDefaultConfig(ctx http.Context) http.Response {
|
||||
index, err := io.Read("/www/server/openresty/html/index.html")
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
stop, err := io.Read("/www/server/openresty/html/stop.html")
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"index": index,
|
||||
"stop": stop,
|
||||
})
|
||||
}
|
||||
|
||||
// SaveDefaultConfig
|
||||
//
|
||||
// @Summary 保存默认配置
|
||||
// @Tags 网站
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body map[string]string true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/website/defaultConfig [post]
|
||||
func (r *WebsiteController) SaveDefaultConfig(ctx http.Context) http.Response {
|
||||
index := ctx.Request().Input("index")
|
||||
stop := ctx.Request().Input("stop")
|
||||
|
||||
if err := io.Write("/www/server/openresty/html/index.html", index, 0644); err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("保存默认首页配置失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
if err := io.Write("/www/server/openresty/html/stop.html", stop, 0644); err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("保存默认停止页配置失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetConfig
|
||||
//
|
||||
// @Summary 获取网站配置
|
||||
// @Tags 网站
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param id path int true "网站 ID"
|
||||
// @Success 200 {object} SuccessResponse{data=types.WebsiteAdd}
|
||||
// @Router /panel/websites/{id}/config [get]
|
||||
func (r *WebsiteController) GetConfig(ctx http.Context) http.Response {
|
||||
var idRequest requests.ID
|
||||
sanitize := SanitizeRequest(ctx, &idRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
config, err := r.website.GetConfig(idRequest.ID)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{
|
||||
"id": idRequest.ID,
|
||||
"error": err.Error(),
|
||||
}).Info("获取网站配置失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveConfig
|
||||
//
|
||||
// @Summary 保存网站配置
|
||||
// @Tags 网站
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param id path int true "网站 ID"
|
||||
// @Param data body requests.SaveConfig true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/websites/{id}/config [post]
|
||||
func (r *WebsiteController) SaveConfig(ctx http.Context) http.Response {
|
||||
var saveConfigRequest requests.SaveConfig
|
||||
sanitize := SanitizeRequest(ctx, &saveConfigRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
err := r.website.SaveConfig(saveConfigRequest)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// ClearLog
|
||||
//
|
||||
// @Summary 清空网站日志
|
||||
// @Tags 网站
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param id path int true "网站 ID"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/websites/{id}/log [delete]
|
||||
func (r *WebsiteController) ClearLog(ctx http.Context) http.Response {
|
||||
var idRequest requests.ID
|
||||
sanitize := SanitizeRequest(ctx, &idRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
website := models.Website{}
|
||||
err := facades.Orm().Query().Where("id", idRequest.ID).Get(&website)
|
||||
if err != nil {
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
if err := io.Remove("/www/wwwlogs/" + website.Name + ".log"); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// UpdateRemark
|
||||
//
|
||||
// @Summary 更新网站备注
|
||||
// @Tags 网站
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param id path int true "网站 ID"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/websites/{id}/updateRemark [post]
|
||||
func (r *WebsiteController) UpdateRemark(ctx http.Context) http.Response {
|
||||
var idRequest requests.ID
|
||||
sanitize := SanitizeRequest(ctx, &idRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
website := models.Website{}
|
||||
err := facades.Orm().Query().Where("id", idRequest.ID).Get(&website)
|
||||
if err != nil {
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
website.Remark = ctx.Request().Input("remark")
|
||||
if err = facades.Orm().Query().Save(&website); err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{
|
||||
"id": idRequest.ID,
|
||||
"error": err.Error(),
|
||||
}).Info("更新网站备注失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// BackupList
|
||||
//
|
||||
// @Summary 获取网站备份列表
|
||||
// @Tags 网站
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data query commonrequests.Paginate true "request"
|
||||
// @Success 200 {object} SuccessResponse{data=[]types.BackupFile}
|
||||
// @Router /panel/website/backupList [get]
|
||||
func (r *WebsiteController) BackupList(ctx http.Context) http.Response {
|
||||
var paginateRequest commonrequests.Paginate
|
||||
sanitize := SanitizeRequest(ctx, &paginateRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
backups, err := r.backup.WebsiteList()
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("获取备份列表失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
paged, total := Paginate(ctx, backups)
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateBackup
|
||||
//
|
||||
// @Summary 创建网站备份
|
||||
// @Tags 网站
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param id path int true "网站 ID"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/websites/{id}/createBackup [post]
|
||||
func (r *WebsiteController) CreateBackup(ctx http.Context) http.Response {
|
||||
var idRequest requests.ID
|
||||
sanitize := SanitizeRequest(ctx, &idRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
website := models.Website{}
|
||||
if err := facades.Orm().Query().Where("id", idRequest.ID).Get(&website); err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{
|
||||
"id": idRequest.ID,
|
||||
"error": err.Error(),
|
||||
}).Info("获取网站信息失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
if err := r.backup.WebSiteBackup(website); err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{
|
||||
"id": idRequest.ID,
|
||||
"error": err.Error(),
|
||||
}).Info("备份网站失败")
|
||||
return Error(ctx, http.StatusInternalServerError, "备份网站失败: "+err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// UploadBackup
|
||||
//
|
||||
// @Summary 上传网站备份
|
||||
// @Tags 网站
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param file formData file true "备份文件"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/website/uploadBackup [put]
|
||||
func (r *WebsiteController) UploadBackup(ctx http.Context) http.Response {
|
||||
file, err := ctx.Request().File("file")
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, "上传文件失败")
|
||||
}
|
||||
|
||||
backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/website"
|
||||
if !io.Exists(backupPath) {
|
||||
if err = io.Mkdir(backupPath, 0644); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
name := file.GetClientOriginalName()
|
||||
_, err = file.StoreAs(backupPath, name)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("上传备份失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// RestoreBackup
|
||||
//
|
||||
// @Summary 还原网站备份
|
||||
// @Tags 网站
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param id path int true "网站 ID"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/websites/{id}/restoreBackup [post]
|
||||
func (r *WebsiteController) RestoreBackup(ctx http.Context) http.Response {
|
||||
var restoreBackupRequest requests.RestoreBackup
|
||||
sanitize := SanitizeRequest(ctx, &restoreBackupRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
website := models.Website{}
|
||||
if err := facades.Orm().Query().Where("id", restoreBackupRequest.ID).Get(&website); err != nil {
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
if err := r.backup.WebsiteRestore(website, restoreBackupRequest.Name); err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{
|
||||
"id": restoreBackupRequest.ID,
|
||||
"file": restoreBackupRequest.Name,
|
||||
"error": err.Error(),
|
||||
}).Info("还原网站失败")
|
||||
return Error(ctx, http.StatusInternalServerError, "还原网站失败: "+err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// DeleteBackup
|
||||
//
|
||||
// @Summary 删除网站备份
|
||||
// @Tags 网站
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.DeleteBackup true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/website/deleteBackup [delete]
|
||||
func (r *WebsiteController) DeleteBackup(ctx http.Context) http.Response {
|
||||
var deleteBackupRequest requests.DeleteBackup
|
||||
sanitize := SanitizeRequest(ctx, &deleteBackupRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/website"
|
||||
if !io.Exists(backupPath) {
|
||||
if err := io.Mkdir(backupPath, 0644); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := io.Remove(backupPath + "/" + deleteBackupRequest.Name); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// ResetConfig
|
||||
//
|
||||
// @Summary 重置网站配置
|
||||
// @Tags 网站
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param id path int true "网站 ID"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/websites/{id}/resetConfig [post]
|
||||
func (r *WebsiteController) ResetConfig(ctx http.Context) http.Response {
|
||||
var idRequest requests.ID
|
||||
sanitize := SanitizeRequest(ctx, &idRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
website := models.Website{}
|
||||
if err := facades.Orm().Query().Where("id", idRequest.ID).Get(&website); err != nil {
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
website.Status = true
|
||||
website.Ssl = false
|
||||
if err := facades.Orm().Query().Save(&website); err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{
|
||||
"id": idRequest.ID,
|
||||
"error": err.Error(),
|
||||
}).Info("保存网站配置失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
raw := fmt.Sprintf(`
|
||||
# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别!
|
||||
# 有自定义配置需求的,请将自定义的配置写在各标记位下方。
|
||||
server
|
||||
{
|
||||
# port标记位开始
|
||||
listen 80;
|
||||
# port标记位结束
|
||||
# server_name标记位开始
|
||||
server_name localhost;
|
||||
# server_name标记位结束
|
||||
# index标记位开始
|
||||
index index.php index.html;
|
||||
# index标记位结束
|
||||
# root标记位开始
|
||||
root %s;
|
||||
# root标记位结束
|
||||
|
||||
# ssl标记位开始
|
||||
# ssl标记位结束
|
||||
|
||||
# php标记位开始
|
||||
include enable-php-%d.conf;
|
||||
# php标记位结束
|
||||
|
||||
# waf标记位开始
|
||||
waf off;
|
||||
waf_rule_path /www/server/openresty/ngx_waf/assets/rules/;
|
||||
waf_mode DYNAMIC;
|
||||
waf_cc_deny rate=1000r/m duration=60m;
|
||||
waf_cache capacity=50;
|
||||
# waf标记位结束
|
||||
|
||||
# 错误页配置,可自行设置
|
||||
error_page 404 /404.html;
|
||||
#error_page 502 /502.html;
|
||||
|
||||
# acme证书签发配置,不可修改
|
||||
include /www/server/vhost/acme/%s.conf;
|
||||
|
||||
# 伪静态规则引入,修改后将导致面板设置的伪静态规则失效
|
||||
include /www/server/vhost/rewrite/%s.conf;
|
||||
|
||||
# 面板默认禁止访问部分敏感目录,可自行修改
|
||||
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn)
|
||||
{
|
||||
return 404;
|
||||
}
|
||||
# 面板默认不记录静态资源的访问日志并开启1小时浏览器缓存,可自行修改
|
||||
location ~ .*\.(js|css)$
|
||||
{
|
||||
expires 1h;
|
||||
error_log /dev/null;
|
||||
access_log /dev/null;
|
||||
}
|
||||
|
||||
access_log /www/wwwlogs/%s.log;
|
||||
error_log /www/wwwlogs/%s.log;
|
||||
}
|
||||
|
||||
`, website.Path, website.Php, website.Name, website.Name, website.Name, website.Name)
|
||||
if err := io.Write("/www/server/vhost/"+website.Name+".conf", raw, 0644); err != nil {
|
||||
return nil
|
||||
}
|
||||
if err := io.Write("/www/server/vhost/rewrite/"+website.Name+".conf", "", 0644); err != nil {
|
||||
return nil
|
||||
}
|
||||
if err := io.Write("/www/server/vhost/acme/"+website.Name+".conf", "", 0644); err != nil {
|
||||
return nil
|
||||
}
|
||||
if err := systemctl.Reload("openresty"); err != nil {
|
||||
_, err = shell.Execf("openresty -t")
|
||||
return Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载OpenResty失败: %v", err))
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Status
|
||||
//
|
||||
// @Summary 获取网站状态
|
||||
// @Tags 网站
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param id path int true "网站 ID"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/websites/{id}/status [post]
|
||||
func (r *WebsiteController) Status(ctx http.Context) http.Response {
|
||||
var idRequest requests.ID
|
||||
sanitize := SanitizeRequest(ctx, &idRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
website := models.Website{}
|
||||
if err := facades.Orm().Query().Where("id", idRequest.ID).Get(&website); err != nil {
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
website.Status = ctx.Request().InputBool("status")
|
||||
if err := facades.Orm().Query().Save(&website); err != nil {
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
raw, err := io.Read("/www/server/vhost/" + website.Name + ".conf")
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
// 运行目录
|
||||
rootConfig := str.Cut(raw, "# root标记位开始\n", "# root标记位结束")
|
||||
match := regexp.MustCompile(`root\s+(.+);`).FindStringSubmatch(rootConfig)
|
||||
if len(match) == 2 {
|
||||
if website.Status {
|
||||
root := regexp.MustCompile(`# root\s+(.+);`).FindStringSubmatch(rootConfig)
|
||||
raw = strings.ReplaceAll(raw, rootConfig, " root "+root[1]+";\n ")
|
||||
} else {
|
||||
raw = strings.ReplaceAll(raw, rootConfig, " root /www/server/openresty/html;\n # root "+match[1]+";\n ")
|
||||
}
|
||||
}
|
||||
|
||||
// 默认文件
|
||||
indexConfig := str.Cut(raw, "# index标记位开始\n", "# index标记位结束")
|
||||
match = regexp.MustCompile(`index\s+(.+);`).FindStringSubmatch(indexConfig)
|
||||
if len(match) == 2 {
|
||||
if website.Status {
|
||||
index := regexp.MustCompile(`# index\s+(.+);`).FindStringSubmatch(indexConfig)
|
||||
raw = strings.ReplaceAll(raw, indexConfig, " index "+index[1]+";\n ")
|
||||
} else {
|
||||
raw = strings.ReplaceAll(raw, indexConfig, " index stop.html;\n # index "+match[1]+";\n ")
|
||||
}
|
||||
}
|
||||
|
||||
if err = io.Write("/www/server/vhost/"+website.Name+".conf", raw, 0644); err != nil {
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
if err = systemctl.Reload("openresty"); err != nil {
|
||||
_, err = shell.Execf("openresty -t")
|
||||
return Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载OpenResty失败: %v", err))
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
|
||||
"github.com/TheTNB/panel/app/http/middleware"
|
||||
)
|
||||
|
||||
type Kernel struct {
|
||||
}
|
||||
|
||||
// The application's global HTTP middleware stack.
|
||||
// These middleware are run during every request to your application.
|
||||
func (kernel Kernel) Middleware() []http.Middleware {
|
||||
return []http.Middleware{
|
||||
middleware.Log(),
|
||||
middleware.Status(),
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/goravel/framework/auth"
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
)
|
||||
|
||||
// Jwt 确保通过 JWT 鉴权
|
||||
func Jwt() http.Middleware {
|
||||
return func(ctx http.Context) {
|
||||
translate := facades.Lang(ctx)
|
||||
token := ctx.Request().Header("Authorization", ctx.Request().Header("Sec-WebSocket-Protocol"))
|
||||
if len(token) == 0 {
|
||||
ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{
|
||||
"message": translate.Get("auth.token.missing"),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// JWT 鉴权
|
||||
if _, err := facades.Auth(ctx).Parse(token); err != nil {
|
||||
if errors.Is(err, auth.ErrorTokenExpired) {
|
||||
token, err = facades.Auth(ctx).Refresh()
|
||||
if err != nil {
|
||||
// 到达刷新时间上限
|
||||
ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{
|
||||
"message": translate.Get("auth.token.expired"),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
token = "Bearer " + token
|
||||
} else {
|
||||
ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{
|
||||
"message": translate.Get("auth.token.expired"),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Response().Header("Authorization", token)
|
||||
ctx.Request().Next()
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
)
|
||||
|
||||
// Log 记录请求日志
|
||||
func Log() http.Middleware {
|
||||
return func(ctx http.Context) {
|
||||
facades.Log().Channel("http").With(map[string]any{
|
||||
"Method": ctx.Request().Method(),
|
||||
"URL": ctx.Request().FullUrl(),
|
||||
"IP": ctx.Request().Ip(),
|
||||
"UA": ctx.Request().Header("User-Agent"),
|
||||
"Body": ctx.Request().All(),
|
||||
}).Info("HTTP Request")
|
||||
ctx.Request().Next()
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/translation"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"github.com/TheTNB/panel/internal/services"
|
||||
)
|
||||
|
||||
// MustInstall 确保已安装插件
|
||||
func MustInstall() http.Middleware {
|
||||
return func(ctx http.Context) {
|
||||
path := ctx.Request().Path()
|
||||
translate := facades.Lang(ctx)
|
||||
var slug string
|
||||
if strings.HasPrefix(path, "/api/panel/website") {
|
||||
slug = "openresty"
|
||||
} else if strings.HasPrefix(path, "/api/panel/container") {
|
||||
slug = "podman"
|
||||
} else {
|
||||
pathArr := strings.Split(path, "/")
|
||||
if len(pathArr) < 4 {
|
||||
ctx.Request().AbortWithStatusJson(http.StatusForbidden, http.Json{
|
||||
"message": translate.Get("errors.plugin.notExist"),
|
||||
})
|
||||
return
|
||||
}
|
||||
slug = pathArr[3]
|
||||
}
|
||||
|
||||
plugin := services.NewPluginImpl().GetBySlug(slug)
|
||||
installedPlugin := services.NewPluginImpl().GetInstalledBySlug(slug)
|
||||
installedPlugins, err := services.NewPluginImpl().AllInstalled()
|
||||
if err != nil {
|
||||
ctx.Request().AbortWithStatusJson(http.StatusInternalServerError, http.Json{
|
||||
"message": translate.Get("errors.internal"),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if installedPlugin.Slug != plugin.Slug {
|
||||
ctx.Request().AbortWithStatusJson(http.StatusForbidden, http.Json{
|
||||
"message": translate.Get("errors.plugin.notInstalled", translation.Option{
|
||||
Replace: map[string]string{
|
||||
"slug": slug,
|
||||
},
|
||||
}),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
pluginsMap := make(map[string]bool)
|
||||
|
||||
for _, p := range installedPlugins {
|
||||
pluginsMap[p.Slug] = true
|
||||
}
|
||||
|
||||
for _, require := range plugin.Requires {
|
||||
_, requireFound := pluginsMap[require]
|
||||
if !requireFound {
|
||||
ctx.Request().AbortWithStatusJson(http.StatusForbidden, http.Json{
|
||||
"message": translate.Get("errors.plugin.dependent", translation.Option{
|
||||
Replace: map[string]string{
|
||||
"slug": slug,
|
||||
"dependency": require,
|
||||
},
|
||||
}),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, exclude := range plugin.Excludes {
|
||||
_, excludeFound := pluginsMap[exclude]
|
||||
if excludeFound {
|
||||
ctx.Request().AbortWithStatusJson(http.StatusForbidden, http.Json{
|
||||
"message": translate.Get("errors.plugin.incompatible", translation.Option{
|
||||
Replace: map[string]string{
|
||||
"slug": slug,
|
||||
"exclude": exclude,
|
||||
},
|
||||
}),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Request().Next()
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
// Status 检查程序状态
|
||||
func Status() http.Middleware {
|
||||
return func(ctx http.Context) {
|
||||
translate := facades.Lang(ctx)
|
||||
switch types.Status {
|
||||
case types.StatusUpgrade:
|
||||
ctx.Request().AbortWithStatusJson(http.StatusServiceUnavailable, http.Json{
|
||||
"message": translate.Get("status.upgrade"),
|
||||
})
|
||||
return
|
||||
case types.StatusMaintain:
|
||||
ctx.Request().AbortWithStatusJson(http.StatusServiceUnavailable, http.Json{
|
||||
"message": translate.Get("status.maintain"),
|
||||
})
|
||||
return
|
||||
case types.StatusClosed:
|
||||
ctx.Request().AbortWithStatusJson(http.StatusForbidden, http.Json{
|
||||
"message": translate.Get("status.closed"),
|
||||
})
|
||||
return
|
||||
case types.StatusFailed:
|
||||
ctx.Request().AbortWithStatusJson(http.StatusInternalServerError, http.Json{
|
||||
"message": translate.Get("status.failed"),
|
||||
})
|
||||
return
|
||||
default:
|
||||
ctx.Request().Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type CertDeploy struct {
|
||||
ID uint `form:"id" json:"id" filter:"uint"`
|
||||
WebsiteID uint `form:"website_id" json:"website_id" filter:"uint"`
|
||||
}
|
||||
|
||||
func (r *CertDeploy) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *CertDeploy) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"id": "required|uint|min:1|exists:certs,id",
|
||||
"website_id": "required|uint|min:1|exists:websites,id",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *CertDeploy) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *CertDeploy) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *CertDeploy) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type CertShowAndDestroy struct {
|
||||
ID uint `form:"id" json:"id"`
|
||||
}
|
||||
|
||||
func (r *CertShowAndDestroy) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *CertShowAndDestroy) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"id": "required|uint|min:1|exists:certs,id",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *CertShowAndDestroy) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *CertShowAndDestroy) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *CertShowAndDestroy) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type CertStore struct {
|
||||
Type string `form:"type" json:"type"`
|
||||
Domains []string `form:"domains" json:"domains"`
|
||||
AutoRenew bool `form:"auto_renew" json:"auto_renew"`
|
||||
UserID uint `form:"user_id" json:"user_id" filter:"uint"`
|
||||
DNSID uint `form:"dns_id" json:"dns_id" filter:"uint"`
|
||||
WebsiteID uint `form:"website_id" json:"website_id" filter:"uint"`
|
||||
}
|
||||
|
||||
func (r *CertStore) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *CertStore) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"type": "required|in:P256,P384,2048,4096",
|
||||
"domains": "required|slice",
|
||||
"auto_renew": "required|bool",
|
||||
"user_id": "required|uint|exists:cert_users,id",
|
||||
"dns_id": "uint",
|
||||
"website_id": "uint",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *CertStore) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *CertStore) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *CertStore) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type CertUpdate struct {
|
||||
ID uint `form:"id" json:"id"`
|
||||
Type string `form:"type" json:"type"`
|
||||
Domains []string `form:"domains" json:"domains"`
|
||||
AutoRenew bool `form:"auto_renew" json:"auto_renew"`
|
||||
UserID uint `form:"user_id" json:"user_id" filter:"uint"`
|
||||
DNSID uint `form:"dns_id" json:"dns_id" filter:"uint"`
|
||||
WebsiteID uint `form:"website_id" json:"website_id" filter:"uint"`
|
||||
}
|
||||
|
||||
func (r *CertUpdate) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *CertUpdate) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"id": "required|uint|min:1|exists:certs,id",
|
||||
"type": "required|in:P256,P384,2048,4096",
|
||||
"domains": "required|slice",
|
||||
"auto_renew": "required|bool",
|
||||
"user_id": "required|uint|exists:cert_users,id",
|
||||
"dns_id": "uint",
|
||||
"website_id": "uint",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *CertUpdate) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *CertUpdate) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *CertUpdate) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type DNSShowAndDestroy struct {
|
||||
ID uint `form:"id" json:"id"`
|
||||
}
|
||||
|
||||
func (r *DNSShowAndDestroy) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *DNSShowAndDestroy) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"id": "required|uint|min:1|exists:cert_dns,id",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *DNSShowAndDestroy) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *DNSShowAndDestroy) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *DNSShowAndDestroy) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/acme"
|
||||
)
|
||||
|
||||
type DNSStore struct {
|
||||
Type string `form:"type" json:"type"`
|
||||
Name string `form:"name" json:"name"`
|
||||
Data acme.DNSParam `form:"data" json:"data"`
|
||||
}
|
||||
|
||||
func (r *DNSStore) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *DNSStore) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"type": "required|in:dnspod,aliyun,cloudflare",
|
||||
"name": "required",
|
||||
"data": "required",
|
||||
"data.id": "required_if:type,dnspod",
|
||||
"data.token": "required_if:type,dnspod",
|
||||
"data.access_key": "required_if:type,aliyun",
|
||||
"data.secret_key": "required_if:type,aliyun",
|
||||
"data.api_key": "required_if:type,cloudflare",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *DNSStore) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *DNSStore) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *DNSStore) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/acme"
|
||||
)
|
||||
|
||||
type DNSUpdate struct {
|
||||
ID uint `form:"id" json:"id"`
|
||||
Type string `form:"type" json:"type"`
|
||||
Name string `form:"name" json:"name"`
|
||||
Data acme.DNSParam `form:"data" json:"data"`
|
||||
}
|
||||
|
||||
func (r *DNSUpdate) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *DNSUpdate) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"id": "required|uint|min:1|exists:cert_dns,id",
|
||||
"type": "required|in:dnspod,aliyun,cloudflare",
|
||||
"name": "required",
|
||||
"data": "required",
|
||||
"data.id": "required_if:type,dnspod",
|
||||
"data.token": "required_if:type,dnspod",
|
||||
"data.access_key": "required_if:type,aliyun",
|
||||
"data.secret_key": "required_if:type,aliyun",
|
||||
"data.email": "required_if:type,cloudflare",
|
||||
"data.api_key": "required_if:type,cloudflare",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *DNSUpdate) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *DNSUpdate) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *DNSUpdate) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type Obtain struct {
|
||||
ID uint `form:"id" json:"id"`
|
||||
}
|
||||
|
||||
func (r *Obtain) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Obtain) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"id": "required|exists:certs,id",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Obtain) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Obtain) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Obtain) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type Renew struct {
|
||||
ID uint `form:"id" json:"id"`
|
||||
}
|
||||
|
||||
func (r *Renew) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Renew) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"id": "required|exists:certs,id",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Renew) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Renew) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Renew) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type UserShowAndDestroy struct {
|
||||
ID uint `form:"id" json:"id"`
|
||||
}
|
||||
|
||||
func (r *UserShowAndDestroy) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *UserShowAndDestroy) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"id": "required|uint|min:1|exists:cert_users,id",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *UserShowAndDestroy) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *UserShowAndDestroy) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *UserShowAndDestroy) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type UserStore struct {
|
||||
CA string `form:"ca" json:"ca"`
|
||||
Email string `form:"email" json:"email"`
|
||||
Kid string `form:"kid" json:"kid"`
|
||||
HmacEncoded string `form:"hmac_encoded" json:"hmac_encoded"`
|
||||
KeyType string `form:"key_type" json:"key_type"`
|
||||
}
|
||||
|
||||
func (r *UserStore) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *UserStore) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"ca": "required|in:letsencrypt,zerossl,sslcom,google,buypass",
|
||||
"email": "required|email",
|
||||
"kid": "required_if:ca,sslcom,google",
|
||||
"hmac_encoded": "required_if:ca,sslcom,google",
|
||||
"key_type": "required|in:P256,P384,2048,4096",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *UserStore) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *UserStore) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *UserStore) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type UserUpdate struct {
|
||||
ID uint `form:"id" json:"id"`
|
||||
CA string `form:"ca" json:"ca"`
|
||||
Email string `form:"email" json:"email"`
|
||||
Kid string `form:"kid" json:"kid"`
|
||||
HmacEncoded string `form:"hmac_encoded" json:"hmac_encoded"`
|
||||
KeyType string `form:"key_type" json:"key_type"`
|
||||
}
|
||||
|
||||
func (r *UserUpdate) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *UserUpdate) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"id": "required|uint|min:1|exists:cert_users,id",
|
||||
"ca": "required|in:letsencrypt,zerossl,sslcom,google,buypass",
|
||||
"email": "required|email",
|
||||
"kid": "required_if:ca,sslcom,google",
|
||||
"hmac_encoded": "required_if:ca,sslcom,google",
|
||||
"key_type": "required|in:P256,P384,2048,4096",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *UserUpdate) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *UserUpdate) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *UserUpdate) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package commonrequests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type Paginate struct {
|
||||
Page int `form:"page" json:"page" filter:"int"`
|
||||
Limit int `form:"limit" json:"limit" filter:"int"`
|
||||
}
|
||||
|
||||
func (r *Paginate) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Paginate) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"page": "required|int|min:1",
|
||||
"limit": "required|int|min:1",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Paginate) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Paginate) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Paginate) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type ContainerCreate struct {
|
||||
Name string `form:"name" json:"name"`
|
||||
Image string `form:"image" json:"image"`
|
||||
Ports []types.ContainerPort `form:"ports" json:"ports"`
|
||||
Network string `form:"network" json:"network"`
|
||||
Volumes []types.ContainerVolume `form:"volumes" json:"volumes"`
|
||||
Labels []types.KV `form:"labels" json:"labels"`
|
||||
Env []types.KV `form:"env" json:"env"`
|
||||
Entrypoint []string `form:"entrypoint" json:"entrypoint"`
|
||||
Command []string `form:"command" json:"command"`
|
||||
RestartPolicy string `form:"restart_policy" json:"restart_policy"`
|
||||
AutoRemove bool `form:"auto_remove" json:"auto_remove"`
|
||||
Privileged bool `form:"privileged" json:"privileged"`
|
||||
OpenStdin bool `form:"openStdin" json:"open_stdin"`
|
||||
PublishAllPorts bool `form:"publish_all_ports" json:"publish_all_ports"`
|
||||
Tty bool `form:"tty" json:"tty"`
|
||||
CPUShares int64 `form:"cpu_shares" json:"cpu_shares" filter:"int"`
|
||||
CPUs int64 `form:"cpus" json:"cpus" filter:"int"`
|
||||
Memory int64 `form:"memory" json:"memory" filter:"int"`
|
||||
}
|
||||
|
||||
func (r *ContainerCreate) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ContainerCreate) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"name": "required|string",
|
||||
"image": "required|string",
|
||||
"ports": "slice",
|
||||
/*"ports.*.host": "string",
|
||||
"ports.*.host_start": "int",
|
||||
"ports.*.host_end": "int",
|
||||
"ports.*.container_start": "int",
|
||||
"ports.*.container_end": "int",
|
||||
"ports.*.protocol": "string|in:tcp,udp",*/
|
||||
"network": "string",
|
||||
"volumes": "slice",
|
||||
/*"volumes.*.host": "string",
|
||||
"volumes.*.container": "string",
|
||||
"volumes.*.mode": "string|in:ro,rw",*/
|
||||
"labels": "slice",
|
||||
"env": "slice",
|
||||
"entrypoint": "slice",
|
||||
"command": "slice",
|
||||
"restart_policy": "string|in:always,on-failure,unless-stopped,no",
|
||||
"auto_remove": "bool",
|
||||
"privileged": "bool",
|
||||
"open_stdin": "bool",
|
||||
"publish_all_ports": "bool",
|
||||
"tty": "bool",
|
||||
"cpu_shares": "int",
|
||||
"cpus": "int",
|
||||
"memory": "int",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ContainerCreate) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *ContainerCreate) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *ContainerCreate) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type ContainerRename struct {
|
||||
ID string `form:"id" json:"id"`
|
||||
Name string `form:"name" json:"name"`
|
||||
}
|
||||
|
||||
func (r *ContainerRename) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ContainerRename) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"id": "required|string",
|
||||
"name": "required|string",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ContainerRename) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *ContainerRename) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *ContainerRename) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type ContainerUpdate struct {
|
||||
ID string `form:"id" json:"id"`
|
||||
Name string `form:"name" json:"name"`
|
||||
Image string `form:"image" json:"image"`
|
||||
Ports []types.ContainerPort `form:"ports" json:"ports"`
|
||||
Network string `form:"network" json:"network"`
|
||||
Volumes []types.ContainerVolume `form:"volumes" json:"volumes"`
|
||||
Labels []string `form:"labels" json:"labels"`
|
||||
Env []string `form:"env" json:"env"`
|
||||
Entrypoint []string `form:"entrypoint" json:"entrypoint"`
|
||||
Command []string `form:"command" json:"command"`
|
||||
RestartPolicy string `form:"restart_policy" json:"restart_policy"`
|
||||
AutoRemove bool `form:"auto_remove" json:"auto_remove"`
|
||||
Privileged bool `form:"privileged" json:"privileged"`
|
||||
OpenStdin bool `form:"openStdin" json:"open_stdin"`
|
||||
PublishAllPorts bool `form:"publish_all_ports" json:"publish_all_ports"`
|
||||
Tty bool `form:"tty" json:"tty"`
|
||||
CPUShares int64 `form:"cpu_shares" json:"cpu_shares"`
|
||||
CPUs int64 `form:"cpus" json:"cpus"`
|
||||
Memory int64 `form:"memory" json:"memory"`
|
||||
}
|
||||
|
||||
func (r *ContainerUpdate) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ContainerUpdate) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"id": "required|string",
|
||||
"name": "required|string",
|
||||
"image": "required|string",
|
||||
"ports": "slice",
|
||||
"network": "string",
|
||||
"volumes": "slice",
|
||||
"labels": "slice",
|
||||
"env": "slice",
|
||||
"entrypoint": "slice",
|
||||
"command": "slice",
|
||||
"restart_policy": "string|in:always,on-failure,unless-stopped,no",
|
||||
"auto_remove": "bool",
|
||||
"privileged": "bool",
|
||||
"open_stdin": "bool",
|
||||
"publish_all_ports": "bool",
|
||||
"tty": "bool",
|
||||
"cpu_shares": "int",
|
||||
"cpus": "int",
|
||||
"memory": "int",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ContainerUpdate) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *ContainerUpdate) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *ContainerUpdate) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type ID struct {
|
||||
ID string `form:"id" json:"id"`
|
||||
}
|
||||
|
||||
func (r *ID) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ID) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"id": "required|string",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ID) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *ID) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *ID) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type ImagePull struct {
|
||||
Name string `form:"name" json:"name"`
|
||||
Auth bool `form:"auth" json:"auth"`
|
||||
Username string `form:"username" json:"username"`
|
||||
Password string `form:"password" json:"password"`
|
||||
}
|
||||
|
||||
func (r *ImagePull) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ImagePull) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"name": "required|string",
|
||||
"auth": "bool",
|
||||
"username": "string",
|
||||
"password": "string",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ImagePull) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *ImagePull) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *ImagePull) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type NetworkConnectDisConnect struct {
|
||||
Network string `form:"network" json:"network"`
|
||||
Container string `form:"container" json:"container"`
|
||||
}
|
||||
|
||||
func (r *NetworkConnectDisConnect) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *NetworkConnectDisConnect) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"network": "required|string",
|
||||
"container": "required|string",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *NetworkConnectDisConnect) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *NetworkConnectDisConnect) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *NetworkConnectDisConnect) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type NetworkCreate struct {
|
||||
Name string `form:"name" json:"name"`
|
||||
Driver string `form:"driver" json:"driver"`
|
||||
Ipv4 types.ContainerNetwork `form:"ipv4" json:"ipv4"`
|
||||
Ipv6 types.ContainerNetwork `form:"ipv6" json:"ipv6"`
|
||||
Labels []types.KV `form:"labels" json:"labels"`
|
||||
Options []types.KV `form:"options" json:"options"`
|
||||
}
|
||||
|
||||
func (r *NetworkCreate) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *NetworkCreate) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"name": "required|string",
|
||||
"driver": "required|string|in:bridge,overlay,macvlan,ipvlan",
|
||||
"ipv4": "required",
|
||||
"ipv6": "required",
|
||||
"labels": "slice",
|
||||
"options": "slice",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *NetworkCreate) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *NetworkCreate) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *NetworkCreate) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type VolumeCreate struct {
|
||||
Name string `form:"name" json:"name"`
|
||||
Driver string `form:"driver" json:"driver"`
|
||||
Labels []types.KV `form:"labels" json:"labels"`
|
||||
Options []types.KV `form:"options" json:"options"`
|
||||
}
|
||||
|
||||
func (r *VolumeCreate) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *VolumeCreate) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"name": "required|string",
|
||||
"driver": "required|string|in:local",
|
||||
"labels": "slice",
|
||||
"options": "slice",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *VolumeCreate) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *VolumeCreate) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *VolumeCreate) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type Archive struct {
|
||||
Paths []string `form:"paths" json:"paths"`
|
||||
File string `form:"file" json:"file"`
|
||||
}
|
||||
|
||||
func (r *Archive) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Archive) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"paths": "array",
|
||||
"paths.*": `regex:^/.*$|path_exists`,
|
||||
"file": `regex:^/.*$|path_not_exists`,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Archive) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Archive) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Archive) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type Copy struct {
|
||||
Source string `form:"source" json:"source"`
|
||||
Target string `form:"target" json:"target"`
|
||||
}
|
||||
|
||||
func (r *Copy) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Copy) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"source": `regex:^/.*$|path_exists`,
|
||||
"target": `regex:^/.*$`,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Copy) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Copy) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Copy) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type Exist struct {
|
||||
Path string `form:"path" json:"path"`
|
||||
}
|
||||
|
||||
func (r *Exist) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Exist) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"path": `regex:^/.*$|path_exists`,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Exist) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Exist) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Exist) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type Move struct {
|
||||
Source string `form:"source" json:"source"`
|
||||
Target string `form:"target" json:"target"`
|
||||
}
|
||||
|
||||
func (r *Move) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Move) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"source": `regex:^/.*$|path_exists`,
|
||||
"target": `regex:^/.*$`,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Move) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Move) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Move) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type NotExist struct {
|
||||
Path string `form:"path" json:"path"`
|
||||
}
|
||||
|
||||
func (r *NotExist) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *NotExist) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"path": `regex:^/.*$|path_not_exists`,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *NotExist) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *NotExist) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *NotExist) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type Permission struct {
|
||||
Path string `form:"path" json:"path"`
|
||||
Mode string `form:"mode" json:"mode"`
|
||||
Owner string `form:"owner" json:"owner"`
|
||||
Group string `form:"group" json:"group"`
|
||||
}
|
||||
|
||||
func (r *Permission) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Permission) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"path": `regex:^/.*$|path_exists`,
|
||||
"mode": "regex:^0[0-7]{3}$",
|
||||
"owner": "regex:^[a-zA-Z0-9_-]+$",
|
||||
"group": "regex:^[a-zA-Z0-9_-]+$",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Permission) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Permission) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Permission) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type Save struct {
|
||||
Path string `form:"path" json:"path"`
|
||||
Content string `form:"content" json:"content"`
|
||||
}
|
||||
|
||||
func (r *Save) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Save) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"path": `regex:^/.*$|path_exists`,
|
||||
"content": "required|string",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Save) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Save) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Save) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type Search struct {
|
||||
Path string `form:"path" json:"path"`
|
||||
KeyWord string `form:"keyword" json:"keyword"`
|
||||
}
|
||||
|
||||
func (r *Search) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Search) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"path": `regex:^/.*$|path_exists`,
|
||||
"keyword": "required|string",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Search) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Search) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Search) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type UnArchive struct {
|
||||
File string `form:"file" json:"file"`
|
||||
Path string `form:"path" json:"path"`
|
||||
}
|
||||
|
||||
func (r *UnArchive) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *UnArchive) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"file": `regex:^/.*$|path_exists`,
|
||||
"path": `regex:^/.*$`,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *UnArchive) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *UnArchive) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *UnArchive) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"mime/multipart"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type Upload struct {
|
||||
Path string `form:"path" json:"path"`
|
||||
File *multipart.FileHeader `form:"file" json:"file"`
|
||||
}
|
||||
|
||||
func (r *Upload) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Upload) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"path": `regex:^/.*$`,
|
||||
"file": "required",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Upload) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Upload) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Upload) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
Service string `form:"service" json:"service"`
|
||||
}
|
||||
|
||||
func (r *Service) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Service) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"service": "required|string|in:frps,frpc",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Service) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Service) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Service) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||