Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
619a2817ce | ||
|
|
12a33ee9fc | ||
|
|
26302ca930 | ||
|
|
7e85555ba7 | ||
|
|
2546676f6a | ||
|
|
93b7edf5c4 | ||
|
|
571e432263 | ||
|
|
580b158567 | ||
|
|
395aa665d7 | ||
|
|
a80e54e0b3 | ||
|
|
8ccd41b551 | ||
|
|
9d6a3a8a0d | ||
|
|
a9d1b4b863 | ||
|
|
459d664a60 | ||
|
|
5f10b0156c | ||
|
|
348c07f847 | ||
|
|
934b1894c4 | ||
|
|
a660f4af93 | ||
|
|
12c0d39b13 | ||
|
|
bc6de68006 | ||
|
|
52bb753594 | ||
|
|
ed6b763d06 | ||
|
|
07e421afea | ||
|
|
71352841bf | ||
|
|
91e8fcbb24 | ||
|
|
5b67a85624 | ||
|
|
b49efa0d5a | ||
|
|
abeb585a0d | ||
|
|
cf621f1cc9 | ||
|
|
389a494e00 | ||
|
|
a9c55dc23b | ||
|
|
3d35b7e71b | ||
|
|
6f78146711 | ||
|
|
8afa47c351 | ||
|
|
20838cfc3e | ||
|
|
d00acd6d2f | ||
|
|
0b58a36779 | ||
|
|
54bc98e9c1 | ||
|
|
836daf2ad9 | ||
|
|
9a8cd9bd87 | ||
|
|
c555d91503 | ||
|
|
e0e7c1bcc4 | ||
|
|
4bf733beec | ||
|
|
e93f23c943 | ||
|
|
5384f4d9f2 | ||
|
|
3c0a97c3cc | ||
|
|
129db6cf4e | ||
|
|
c6bfa5652f | ||
|
|
780472d83e | ||
|
|
0d02e3f15a | ||
|
|
bf82f22d0f | ||
|
|
e18f182ce6 | ||
|
|
761c26b587 |
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/TeaOSLab/EdgeAdmin/internal/apps"
|
"github.com/TeaOSLab/EdgeAdmin/internal/apps"
|
||||||
"github.com/TeaOSLab/EdgeAdmin/internal/configs"
|
"github.com/TeaOSLab/EdgeAdmin/internal/configs"
|
||||||
@@ -24,7 +25,7 @@ func main() {
|
|||||||
var app = apps.NewAppCmd().
|
var app = apps.NewAppCmd().
|
||||||
Version(teaconst.Version).
|
Version(teaconst.Version).
|
||||||
Product(teaconst.ProductName).
|
Product(teaconst.ProductName).
|
||||||
Usage(teaconst.ProcessName+" [-v|start|stop|restart|service|daemon|reset|recover|demo|upgrade]").
|
Usage(teaconst.ProcessName+" [-h|-v|start|stop|restart|service|daemon|reset|recover|demo|upgrade]").
|
||||||
Usage(teaconst.ProcessName+" [dev|prod]").
|
Usage(teaconst.ProcessName+" [dev|prod]").
|
||||||
Option("-h", "show this help").
|
Option("-h", "show this help").
|
||||||
Option("-v", "show version").
|
Option("-v", "show version").
|
||||||
@@ -38,7 +39,7 @@ func main() {
|
|||||||
Option("demo", "switch to demo mode").
|
Option("demo", "switch to demo mode").
|
||||||
Option("dev", "switch to 'dev' mode").
|
Option("dev", "switch to 'dev' mode").
|
||||||
Option("prod", "switch to 'prod' mode").
|
Option("prod", "switch to 'prod' mode").
|
||||||
Option("upgrade", "upgrade from official site")
|
Option("upgrade [--url=URL]", "upgrade from official site or an url")
|
||||||
|
|
||||||
app.On("daemon", func() {
|
app.On("daemon", func() {
|
||||||
nodes.NewAdminNode().Daemon()
|
nodes.NewAdminNode().Daemon()
|
||||||
@@ -138,7 +139,12 @@ func main() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
app.On("upgrade", func() {
|
app.On("upgrade", func() {
|
||||||
var manager = utils.NewUpgradeManager("admin")
|
var downloadURL = ""
|
||||||
|
var flagSet = flag.NewFlagSet("", flag.ContinueOnError)
|
||||||
|
flagSet.StringVar(&downloadURL, "url", "", "new version download url")
|
||||||
|
_ = flagSet.Parse(os.Args[2:])
|
||||||
|
|
||||||
|
var manager = utils.NewUpgradeManager("admin", downloadURL)
|
||||||
log.Println("checking latest version ...")
|
log.Println("checking latest version ...")
|
||||||
var ticker = time.NewTicker(1 * time.Second)
|
var ticker = time.NewTicker(1 * time.Second)
|
||||||
go func() {
|
go func() {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
LABEL maintainer="iwind.liu@gmail.com"
|
LABEL maintainer="iwind.liu@gmail.com"
|
||||||
ENV TZ "Asia/Shanghai"
|
ENV TZ "Asia/Shanghai"
|
||||||
ENV VERSION 0.6.1
|
ENV VERSION 0.6.3
|
||||||
ENV ROOT_DIR /usr/local/goedge
|
ENV ROOT_DIR /usr/local/goedge
|
||||||
ENV TAR_FILE edge-admin-linux-amd64-plus-v${VERSION}.zip
|
ENV TAR_FILE edge-admin-linux-amd64-plus-v${VERSION}.zip
|
||||||
ENV TAR_URL "https://dl.goedge.cn/edge/v${VERSION}/edge-admin-linux-amd64-plus-v${VERSION}.zip"
|
ENV TAR_URL "https://dl.goedge.cn/edge/v${VERSION}/edge-admin-linux-amd64-plus-v${VERSION}.zip"
|
||||||
|
|||||||
11
go.mod
11
go.mod
@@ -8,14 +8,14 @@ require (
|
|||||||
github.com/TeaOSLab/EdgeCommon v0.0.0-00010101000000-000000000000
|
github.com/TeaOSLab/EdgeCommon v0.0.0-00010101000000-000000000000
|
||||||
github.com/cespare/xxhash v1.1.0
|
github.com/cespare/xxhash v1.1.0
|
||||||
github.com/go-sql-driver/mysql v1.5.0
|
github.com/go-sql-driver/mysql v1.5.0
|
||||||
github.com/iwind/TeaGo v0.0.0-20220304043459-0dd944a5b475
|
github.com/iwind/TeaGo v0.0.0-20230304012706-c1f4a4e27470
|
||||||
github.com/iwind/gosock v0.0.0-20211103081026-ee4652210ca4
|
github.com/iwind/gosock v0.0.0-20211103081026-ee4652210ca4
|
||||||
github.com/miekg/dns v1.1.43
|
github.com/miekg/dns v1.1.43
|
||||||
github.com/shirou/gopsutil/v3 v3.22.5
|
github.com/shirou/gopsutil/v3 v3.22.5
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
github.com/tealeg/xlsx/v3 v3.2.3
|
github.com/tealeg/xlsx/v3 v3.2.3
|
||||||
github.com/xlzd/gotp v0.0.0-20181030022105-c8557ba2c119
|
github.com/xlzd/gotp v0.0.0-20181030022105-c8557ba2c119
|
||||||
golang.org/x/sys v0.2.0
|
golang.org/x/sys v0.5.0
|
||||||
google.golang.org/grpc v1.45.0
|
google.golang.org/grpc v1.45.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
@@ -26,18 +26,15 @@ require (
|
|||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
github.com/google/btree v1.0.0 // indirect
|
github.com/google/btree v1.0.0 // indirect
|
||||||
github.com/google/go-cmp v0.5.8 // indirect
|
github.com/google/go-cmp v0.5.8 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
|
||||||
github.com/kr/pretty v0.2.1 // indirect
|
github.com/kr/pretty v0.2.1 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
|
||||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||||
github.com/rogpeppe/fastuuid v1.2.0 // indirect
|
github.com/rogpeppe/fastuuid v1.2.0 // indirect
|
||||||
github.com/shabbyrobe/xmlwriter v0.0.0-20200208144257-9fca06d00ffa // indirect
|
github.com/shabbyrobe/xmlwriter v0.0.0-20200208144257-9fca06d00ffa // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||||
golang.org/x/net v0.2.0 // indirect
|
golang.org/x/net v0.7.0 // indirect
|
||||||
golang.org/x/text v0.4.0 // indirect
|
golang.org/x/text v0.7.0 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20220317150908-0efb43f6373e // indirect
|
google.golang.org/genproto v0.0.0-20220317150908-0efb43f6373e // indirect
|
||||||
google.golang.org/protobuf v1.27.1 // indirect
|
google.golang.org/protobuf v1.27.1 // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||||
|
|||||||
30
go.sum
30
go.sum
@@ -21,7 +21,6 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH
|
|||||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200624174652-8d2f3be8b2d9/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
github.com/dgryski/go-rendezvous v0.0.0-20200624174652-8d2f3be8b2d9/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
@@ -75,13 +74,15 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
|
|||||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/iwind/TeaGo v0.0.0-20210411134150-ddf57e240c2f/go.mod h1:KU4mS7QNiZ7QWEuDBk1zw0/Q2LrAPZv3tycEFBsuUwc=
|
github.com/iwind/TeaGo v0.0.0-20210411134150-ddf57e240c2f/go.mod h1:KU4mS7QNiZ7QWEuDBk1zw0/Q2LrAPZv3tycEFBsuUwc=
|
||||||
github.com/iwind/TeaGo v0.0.0-20220304043459-0dd944a5b475 h1:EseyfFaQOjWanGiby9KMw7PjDBMg/95tLDgIw/ns0Cw=
|
github.com/iwind/TeaGo v0.0.0-20230207032553-d6dcde0cd518 h1:zuWjQ57zc67ZSTHpxNK95JYoa9Ph/JRSnapsTY/hlhQ=
|
||||||
github.com/iwind/TeaGo v0.0.0-20220304043459-0dd944a5b475/go.mod h1:HRHK0zoC/og3c9/hKosD9yYVMTnnzm3PgXUdhRYHaLc=
|
github.com/iwind/TeaGo v0.0.0-20230207032553-d6dcde0cd518/go.mod h1:fi/Pq+/5m2HZoseM+39dMF57ANXRt6w4PkGu3NXPc5s=
|
||||||
|
github.com/iwind/TeaGo v0.0.0-20230303070415-9d0689db6456 h1:xv3AVaxuwjThkBDptAfsFSmuHQIrRrvt8BRaekWnsvs=
|
||||||
|
github.com/iwind/TeaGo v0.0.0-20230303070415-9d0689db6456/go.mod h1:fi/Pq+/5m2HZoseM+39dMF57ANXRt6w4PkGu3NXPc5s=
|
||||||
|
github.com/iwind/TeaGo v0.0.0-20230304012706-c1f4a4e27470 h1:TuRxvKRv9PxKVijWOkUnZm5TeanQqWGUJyPx9u6cra4=
|
||||||
|
github.com/iwind/TeaGo v0.0.0-20230304012706-c1f4a4e27470/go.mod h1:fi/Pq+/5m2HZoseM+39dMF57ANXRt6w4PkGu3NXPc5s=
|
||||||
github.com/iwind/gosock v0.0.0-20211103081026-ee4652210ca4 h1:VWGsCqTzObdlbf7UUE3oceIpcEKi4C/YBUszQXk118A=
|
github.com/iwind/gosock v0.0.0-20211103081026-ee4652210ca4 h1:VWGsCqTzObdlbf7UUE3oceIpcEKi4C/YBUszQXk118A=
|
||||||
github.com/iwind/gosock v0.0.0-20211103081026-ee4652210ca4/go.mod h1:H5Q7SXwbx3a97ecJkaS2sD77gspzE7HFUafBO0peEyA=
|
github.com/iwind/gosock v0.0.0-20211103081026-ee4652210ca4/go.mod h1:H5Q7SXwbx3a97ecJkaS2sD77gspzE7HFUafBO0peEyA=
|
||||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
@@ -93,11 +94,7 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2
|
|||||||
github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg=
|
github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg=
|
||||||
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
|
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
@@ -112,7 +109,6 @@ github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+v
|
|||||||
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||||
github.com/pkg/profile v1.5.0 h1:042Buzk+NhDI+DeSAA62RwJL8VAuZUMQZUjCsRz1Mug=
|
github.com/pkg/profile v1.5.0 h1:042Buzk+NhDI+DeSAA62RwJL8VAuZUMQZUjCsRz1Mug=
|
||||||
github.com/pkg/profile v1.5.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
|
github.com/pkg/profile v1.5.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
@@ -132,7 +128,6 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
|||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/tealeg/xlsx/v3 v3.2.3 h1:MXnVh+9Y8cUglowItTy2HL3Kv6z+q/0aNjeKuTsVqZQ=
|
github.com/tealeg/xlsx/v3 v3.2.3 h1:MXnVh+9Y8cUglowItTy2HL3Kv6z+q/0aNjeKuTsVqZQ=
|
||||||
github.com/tealeg/xlsx/v3 v3.2.3/go.mod h1:0hGmAEoZ48SS1ZAE6eqZJkJVXgOMY+8a33vjXa8S8HA=
|
github.com/tealeg/xlsx/v3 v3.2.3/go.mod h1:0hGmAEoZ48SS1ZAE6eqZJkJVXgOMY+8a33vjXa8S8HA=
|
||||||
@@ -171,8 +166,8 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/
|
|||||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
|
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -200,15 +195,15 @@ golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
@@ -263,7 +258,6 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|||||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ func (this *AppCmd) runStop() {
|
|||||||
fmt.Println(this.product+" stopped ok, pid:", types.String(pid))
|
fmt.Println(this.product+" stopped ok, pid:", types.String(pid))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重启
|
// RunRestart 重启
|
||||||
func (this *AppCmd) RunRestart() {
|
func (this *AppCmd) RunRestart() {
|
||||||
this.runStop()
|
this.runStop()
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
|
|||||||
@@ -155,3 +155,15 @@ func (this *APIConfig) WriteFile(path string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clone 克隆当前配置
|
||||||
|
func (this *APIConfig) Clone() *APIConfig {
|
||||||
|
return &APIConfig{
|
||||||
|
RPC: struct {
|
||||||
|
Endpoints []string `yaml:"endpoints"`
|
||||||
|
DisableUpdate bool `yaml:"disableUpdate"`
|
||||||
|
}{},
|
||||||
|
NodeId: this.NodeId,
|
||||||
|
Secret: this.Secret,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
|
||||||
|
|
||||||
package configs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/iwind/TeaGo/Tea"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
var plusConfigFile = "plus.cache.json"
|
|
||||||
|
|
||||||
type PlusConfig struct {
|
|
||||||
IsPlus bool `json:"isPlus"`
|
|
||||||
Components []string `json:"components"`
|
|
||||||
DayTo string `json:"dayTo"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadPlusConfig() *PlusConfig {
|
|
||||||
data, err := os.ReadFile(Tea.ConfigFile(plusConfigFile))
|
|
||||||
if err != nil {
|
|
||||||
return &PlusConfig{IsPlus: false}
|
|
||||||
}
|
|
||||||
var config = &PlusConfig{IsPlus: false}
|
|
||||||
err = json.Unmarshal(data, config)
|
|
||||||
if err != nil {
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
func WritePlusConfig(config *PlusConfig) error {
|
|
||||||
configJSON, err := json.Marshal(config)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = os.WriteFile(Tea.ConfigFile(plusConfigFile), configJSON, 0777)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package teaconst
|
package teaconst
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Version = "0.6.2"
|
Version = "0.6.4"
|
||||||
|
|
||||||
APINodeVersion = "0.6.2"
|
APINodeVersion = "0.6.4"
|
||||||
|
|
||||||
ProductName = "Edge Admin"
|
ProductName = "Edge Admin"
|
||||||
ProcessName = "edge-admin"
|
ProcessName = "edge-admin"
|
||||||
@@ -18,5 +18,5 @@ const (
|
|||||||
CookieSID = "edgesid"
|
CookieSID = "edgesid"
|
||||||
|
|
||||||
SystemdServiceName = "edge-admin"
|
SystemdServiceName = "edge-admin"
|
||||||
UpdatesURL = "https://goedge.cn/api/boot/versions?os=${os}&arch=${arch}"
|
UpdatesURL = "https://goedge.cn/api/boot/versions?os=${os}&arch=${arch}&version=${version}"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"github.com/iwind/TeaGo/logs"
|
"github.com/iwind/TeaGo/logs"
|
||||||
"github.com/iwind/TeaGo/maps"
|
"github.com/iwind/TeaGo/maps"
|
||||||
"github.com/iwind/TeaGo/rands"
|
"github.com/iwind/TeaGo/rands"
|
||||||
"github.com/iwind/TeaGo/sessions"
|
|
||||||
"github.com/iwind/TeaGo/types"
|
"github.com/iwind/TeaGo/types"
|
||||||
"github.com/iwind/gosock/pkg/gosock"
|
"github.com/iwind/gosock/pkg/gosock"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
@@ -21,6 +20,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -85,10 +86,15 @@ func (this *AdminNode) Run() {
|
|||||||
this.startAPINode()
|
this.startAPINode()
|
||||||
|
|
||||||
// 启动Web服务
|
// 启动Web服务
|
||||||
|
sessionManager, err := NewSessionManager()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("start session failed: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
TeaGo.NewServer(false).
|
TeaGo.NewServer(false).
|
||||||
AccessLog(false).
|
AccessLog(false).
|
||||||
EndAll().
|
EndAll().
|
||||||
Session(sessions.NewFileSessionManager(86400, secret), teaconst.CookieSID).
|
Session(sessionManager, teaconst.CookieSID).
|
||||||
ReadHeaderTimeout(3 * time.Second).
|
ReadHeaderTimeout(3 * time.Second).
|
||||||
ReadTimeout(1200 * time.Second).
|
ReadTimeout(1200 * time.Second).
|
||||||
Start()
|
Start()
|
||||||
@@ -360,6 +366,16 @@ func (this *AdminNode) listenSock() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 停止当前目录下的API节点
|
||||||
|
var apiSock = gosock.NewTmpSock("edge-api")
|
||||||
|
apiReply, err := apiSock.Send(&gosock.Command{Code: "info"})
|
||||||
|
if err == nil {
|
||||||
|
adminExe, _ := os.Executable()
|
||||||
|
if len(adminExe) > 0 && apiReply != nil && strings.HasPrefix(maps.NewMap(apiReply.Params).GetString("path"), filepath.Dir(filepath.Dir(adminExe))) {
|
||||||
|
_, _ = apiSock.Send(&gosock.Command{Code: "stop"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 退出主进程
|
// 退出主进程
|
||||||
events.Notify(events.EventQuit)
|
events.Notify(events.EventQuit)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
|
|||||||
96
internal/nodes/session_manager.go
Normal file
96
internal/nodes/session_manager.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||||
|
|
||||||
|
package nodes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
|
||||||
|
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||||
|
"github.com/iwind/TeaGo/actions"
|
||||||
|
"github.com/iwind/TeaGo/logs"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SessionManager struct {
|
||||||
|
life uint
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSessionManager() (*SessionManager, error) {
|
||||||
|
return &SessionManager{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *SessionManager) Init(config *actions.SessionConfig) {
|
||||||
|
this.life = config.Life
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *SessionManager) Read(sid string) map[string]string {
|
||||||
|
// 忽略OTP
|
||||||
|
if strings.HasSuffix(sid, "_otp") {
|
||||||
|
return map[string]string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = map[string]string{}
|
||||||
|
|
||||||
|
rpcClient, err := rpc.SharedRPC()
|
||||||
|
if err != nil {
|
||||||
|
return map[string]string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := rpcClient.LoginSessionRPC().FindLoginSession(rpcClient.Context(0), &pb.FindLoginSessionRequest{Sid: sid})
|
||||||
|
if err != nil {
|
||||||
|
logs.Println("SESSION", "read '"+sid+"' failed: "+err.Error())
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
var session = resp.LoginSession
|
||||||
|
if session == nil || len(session.ValuesJSON) == 0 {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(session.ValuesJSON, &result)
|
||||||
|
if err != nil {
|
||||||
|
logs.Println("SESSION", "decode '"+sid+"' values failed: "+err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *SessionManager) WriteItem(sid string, key string, value string) bool {
|
||||||
|
// 忽略OTP
|
||||||
|
if strings.HasSuffix(sid, "_otp") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
rpcClient, err := rpc.SharedRPC()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = rpcClient.LoginSessionRPC().WriteLoginSessionValue(rpcClient.Context(0), &pb.WriteLoginSessionValueRequest{
|
||||||
|
Sid: sid,
|
||||||
|
Key: key,
|
||||||
|
Value: value,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logs.Println("SESSION", "write sid:'"+sid+"' key:'"+key+"' failed: "+err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *SessionManager) Delete(sid string) bool {
|
||||||
|
// 忽略OTP
|
||||||
|
if strings.HasSuffix(sid, "_otp") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
rpcClient, err := rpc.SharedRPC()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, err = rpcClient.LoginSessionRPC().DeleteLoginSession(rpcClient.Context(0), &pb.DeleteLoginSessionRequest{Sid: sid})
|
||||||
|
if err != nil {
|
||||||
|
logs.Println("SESSION", "delete '"+sid+"' failed: "+err.Error())
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -407,6 +407,10 @@ func (this *RPCClient) LoginRPC() pb.LoginServiceClient {
|
|||||||
return pb.NewLoginServiceClient(this.pickConn())
|
return pb.NewLoginServiceClient(this.pickConn())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (this *RPCClient) LoginSessionRPC() pb.LoginSessionServiceClient {
|
||||||
|
return pb.NewLoginSessionServiceClient(this.pickConn())
|
||||||
|
}
|
||||||
|
|
||||||
func (this *RPCClient) NodeTaskRPC() pb.NodeTaskServiceClient {
|
func (this *RPCClient) NodeTaskRPC() pb.NodeTaskServiceClient {
|
||||||
return pb.NewNodeTaskServiceClient(this.pickConn())
|
return pb.NewNodeTaskServiceClient(this.pickConn())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ func (this *CheckUpdatesTask) Loop() error {
|
|||||||
var apiURL = teaconst.UpdatesURL
|
var apiURL = teaconst.UpdatesURL
|
||||||
apiURL = strings.ReplaceAll(apiURL, "${os}", runtime.GOOS)
|
apiURL = strings.ReplaceAll(apiURL, "${os}", runtime.GOOS)
|
||||||
apiURL = strings.ReplaceAll(apiURL, "${arch}", runtime.GOARCH)
|
apiURL = strings.ReplaceAll(apiURL, "${arch}", runtime.GOARCH)
|
||||||
|
apiURL = strings.ReplaceAll(apiURL, "${version}", teaconst.Version)
|
||||||
resp, err := http.Get(apiURL)
|
resp, err := http.Get(apiURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("read api failed: " + err.Error())
|
return errors.New("read api failed: " + err.Error())
|
||||||
|
|||||||
30
internal/utils/apinodeutils/manager.go
Normal file
30
internal/utils/apinodeutils/manager.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||||
|
|
||||||
|
package apinodeutils
|
||||||
|
|
||||||
|
var SharedManager = NewManager()
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
upgraderMap map[int64]*Upgrader
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager() *Manager {
|
||||||
|
return &Manager{
|
||||||
|
upgraderMap: map[int64]*Upgrader{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Manager) AddUpgrader(upgrader *Upgrader) {
|
||||||
|
this.upgraderMap[upgrader.apiNodeId] = upgrader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Manager) FindUpgrader(apiNodeId int64) *Upgrader {
|
||||||
|
return this.upgraderMap[apiNodeId]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Manager) RemoveUpgrader(upgrader *Upgrader) {
|
||||||
|
if upgrader == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
delete(this.upgraderMap, upgrader.apiNodeId)
|
||||||
|
}
|
||||||
201
internal/utils/apinodeutils/upgrader.go
Normal file
201
internal/utils/apinodeutils/upgrader.go
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||||
|
|
||||||
|
package apinodeutils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"compress/gzip"
|
||||||
|
"crypto/md5"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/configs"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
|
||||||
|
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||||
|
"github.com/iwind/TeaGo/Tea"
|
||||||
|
"github.com/iwind/TeaGo/types"
|
||||||
|
stringutil "github.com/iwind/TeaGo/utils/string"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Progress struct {
|
||||||
|
Percent float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type Upgrader struct {
|
||||||
|
progress *Progress
|
||||||
|
apiExe string
|
||||||
|
apiNodeId int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUpgrader(apiNodeId int64) *Upgrader {
|
||||||
|
return &Upgrader{
|
||||||
|
apiExe: apiExe(),
|
||||||
|
progress: &Progress{Percent: 0},
|
||||||
|
apiNodeId: apiNodeId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Upgrader) Upgrade() error {
|
||||||
|
sharedClient, err := rpc.SharedRPC()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
apiNodeResp, err := sharedClient.APINodeRPC().FindEnabledAPINode(sharedClient.Context(0), &pb.FindEnabledAPINodeRequest{ApiNodeId: this.apiNodeId})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var apiNode = apiNodeResp.ApiNode
|
||||||
|
if apiNode == nil {
|
||||||
|
return errors.New("could not find api node with id '" + types.String(this.apiNodeId) + "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
apiConfig, err := configs.LoadAPIConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var newAPIConfig = apiConfig.Clone()
|
||||||
|
newAPIConfig.RPC.Endpoints = apiNode.AccessAddrs
|
||||||
|
|
||||||
|
rpcClient, err := rpc.NewRPCClient(newAPIConfig, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
versionResp, err := rpcClient.APINodeRPC().FindCurrentAPINodeVersion(sharedClient.Context(0), &pb.FindCurrentAPINodeVersionRequest{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !Tea.IsTesting() /** 开发环境下允许突破此限制方便测试 **/ &&
|
||||||
|
(stringutil.VersionCompare(versionResp.Version, "0.6.4" /** 从0.6.4开始支持 **/) < 0 || versionResp.Os != runtime.GOOS || versionResp.Arch != runtime.GOARCH) {
|
||||||
|
return errors.New("could not upgrade api node v" + versionResp.Version + "/" + versionResp.Os + "/" + versionResp.Arch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查本地文件版本
|
||||||
|
canUpgrade, reason := CanUpgrade(versionResp.Version, versionResp.Os, versionResp.Arch)
|
||||||
|
if !canUpgrade {
|
||||||
|
return errors.New(reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
localVersion, err := localVersion()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("lookup version failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查要升级的文件
|
||||||
|
var gzFile = this.apiExe + "." + localVersion + ".gz"
|
||||||
|
|
||||||
|
gzReader, err := os.Open(gzFile)
|
||||||
|
if err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = func() error {
|
||||||
|
// 压缩文件
|
||||||
|
exeReader, err := os.Open(this.apiExe)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = exeReader.Close()
|
||||||
|
}()
|
||||||
|
var tmpGzFile = gzFile + ".tmp"
|
||||||
|
gzFileWriter, err := os.OpenFile(tmpGzFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var gzWriter = gzip.NewWriter(gzFileWriter)
|
||||||
|
defer func() {
|
||||||
|
_ = gzWriter.Close()
|
||||||
|
_ = gzFileWriter.Close()
|
||||||
|
|
||||||
|
_ = os.Rename(tmpGzFile, gzFile)
|
||||||
|
}()
|
||||||
|
_, err = io.Copy(gzWriter, exeReader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
gzReader, err = os.Open(gzFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
_ = gzReader.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 开始上传
|
||||||
|
var hash = md5.New()
|
||||||
|
var buf = make([]byte, 128*4096)
|
||||||
|
var isFirst = true
|
||||||
|
stat, err := gzReader.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var totalSize = stat.Size()
|
||||||
|
if totalSize == 0 {
|
||||||
|
_ = gzReader.Close()
|
||||||
|
_ = os.Remove(gzFile)
|
||||||
|
return errors.New("invalid gz file")
|
||||||
|
}
|
||||||
|
|
||||||
|
var uploadedSize int64 = 0
|
||||||
|
for {
|
||||||
|
n, err := gzReader.Read(buf)
|
||||||
|
if n > 0 {
|
||||||
|
// 计算Hash
|
||||||
|
hash.Write(buf[:n])
|
||||||
|
|
||||||
|
// 上传
|
||||||
|
_, uploadErr := rpcClient.APINodeRPC().UploadAPINodeFile(rpcClient.Context(0), &pb.UploadAPINodeFileRequest{
|
||||||
|
Filename: filepath.Base(this.apiExe),
|
||||||
|
Sum: "",
|
||||||
|
ChunkData: buf[:n],
|
||||||
|
IsFirstChunk: isFirst,
|
||||||
|
IsLastChunk: false,
|
||||||
|
})
|
||||||
|
if uploadErr != nil {
|
||||||
|
return uploadErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// 进度
|
||||||
|
uploadedSize += int64(n)
|
||||||
|
this.progress = &Progress{Percent: float64(uploadedSize*100) / float64(totalSize)}
|
||||||
|
}
|
||||||
|
if isFirst {
|
||||||
|
isFirst = false
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
if err != io.EOF {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err == io.EOF {
|
||||||
|
_, uploadErr := rpcClient.APINodeRPC().UploadAPINodeFile(rpcClient.Context(0), &pb.UploadAPINodeFileRequest{
|
||||||
|
Filename: filepath.Base(this.apiExe),
|
||||||
|
Sum: fmt.Sprintf("%x", hash.Sum(nil)),
|
||||||
|
ChunkData: buf[:n],
|
||||||
|
IsFirstChunk: isFirst,
|
||||||
|
IsLastChunk: true,
|
||||||
|
})
|
||||||
|
if uploadErr != nil {
|
||||||
|
return uploadErr
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Upgrader) Progress() *Progress {
|
||||||
|
return this.progress
|
||||||
|
}
|
||||||
22
internal/utils/apinodeutils/upgrader_test.go
Normal file
22
internal/utils/apinodeutils/upgrader_test.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||||
|
|
||||||
|
package apinodeutils_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/utils/apinodeutils"
|
||||||
|
_ "github.com/iwind/TeaGo/bootstrap"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUpgrader_CanUpgrade(t *testing.T) {
|
||||||
|
t.Log(apinodeutils.CanUpgrade("0.6.3", runtime.GOOS, runtime.GOARCH))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpgrader_Upgrade(t *testing.T) {
|
||||||
|
var upgrader = apinodeutils.NewUpgrader(1)
|
||||||
|
err := upgrader.Upgrade()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
80
internal/utils/apinodeutils/utils.go
Normal file
80
internal/utils/apinodeutils/utils.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||||
|
|
||||||
|
package apinodeutils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
|
||||||
|
"github.com/iwind/TeaGo/Tea"
|
||||||
|
stringutil "github.com/iwind/TeaGo/utils/string"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CanUpgrade(apiVersion string, osName string, arch string) (canUpgrade bool, reason string) {
|
||||||
|
if len(apiVersion) == 0 {
|
||||||
|
return false, "current api version should not be empty"
|
||||||
|
}
|
||||||
|
|
||||||
|
if stringutil.VersionCompare(apiVersion, "0.6.4") < 0 {
|
||||||
|
return false, "api node version must greater than or equal to 0.6.4"
|
||||||
|
}
|
||||||
|
|
||||||
|
if osName != runtime.GOOS {
|
||||||
|
return false, "os not match: " + osName
|
||||||
|
}
|
||||||
|
if arch != runtime.GOARCH {
|
||||||
|
return false, "arch not match: " + arch
|
||||||
|
}
|
||||||
|
|
||||||
|
stat, err := os.Stat(apiExe())
|
||||||
|
if err != nil {
|
||||||
|
return false, "stat error: " + err.Error()
|
||||||
|
}
|
||||||
|
if stat.IsDir() {
|
||||||
|
return false, "is directory"
|
||||||
|
}
|
||||||
|
|
||||||
|
localVersion, err := localVersion()
|
||||||
|
if err != nil {
|
||||||
|
return false, "lookup version failed: " + err.Error()
|
||||||
|
}
|
||||||
|
if localVersion != teaconst.APINodeVersion {
|
||||||
|
return false, "not newest api node"
|
||||||
|
}
|
||||||
|
if stringutil.VersionCompare(localVersion, apiVersion) <= 0 {
|
||||||
|
return false, "need not upgrade, local '" + localVersion + "' vs remote '" + apiVersion + "'"
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func localVersion() (string, error) {
|
||||||
|
var cmd = exec.Command(apiExe(), "-V")
|
||||||
|
var output = &bytes.Buffer{}
|
||||||
|
cmd.Stdout = output
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var localVersion = strings.TrimSpace(output.String())
|
||||||
|
|
||||||
|
// 检查版本号
|
||||||
|
var reg = regexp.MustCompile(`^[\d.]+$`)
|
||||||
|
if !reg.MatchString(localVersion) {
|
||||||
|
return "", errors.New("lookup version failed: " + localVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
return localVersion, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func apiExe() string {
|
||||||
|
return Tea.Root + "/edge-api/bin/edge-api"
|
||||||
|
}
|
||||||
12
internal/utils/dateutils/utils.go
Normal file
12
internal/utils/dateutils/utils.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||||
|
|
||||||
|
package dateutils
|
||||||
|
|
||||||
|
// SplitYmd 分隔Ymd格式的日期
|
||||||
|
// Ymd => Y-m-d
|
||||||
|
func SplitYmd(day string) string {
|
||||||
|
if len(day) != 8 {
|
||||||
|
return day
|
||||||
|
}
|
||||||
|
return day[:4] + "-" + day[4:6] + "-" + day[6:]
|
||||||
|
}
|
||||||
@@ -52,11 +52,14 @@ type UpgradeManager struct {
|
|||||||
writer *UpgradeFileWriter
|
writer *UpgradeFileWriter
|
||||||
body io.ReadCloser
|
body io.ReadCloser
|
||||||
isCancelled bool
|
isCancelled bool
|
||||||
|
|
||||||
|
downloadURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUpgradeManager(component string) *UpgradeManager {
|
func NewUpgradeManager(component string, downloadURL string) *UpgradeManager {
|
||||||
return &UpgradeManager{
|
return &UpgradeManager{
|
||||||
component: component,
|
component: component,
|
||||||
|
downloadURL: downloadURL,
|
||||||
client: &http.Client{
|
client: &http.Client{
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
TLSClientConfig: &tls.Config{
|
TLSClientConfig: &tls.Config{
|
||||||
@@ -96,8 +99,8 @@ func (this *UpgradeManager) Start() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查新版本
|
// 检查新版本
|
||||||
var downloadURL = ""
|
var downloadURL = this.downloadURL
|
||||||
{
|
if len(downloadURL) == 0 {
|
||||||
var url = teaconst.UpdatesURL
|
var url = teaconst.UpdatesURL
|
||||||
var osName = runtime.GOOS
|
var osName = runtime.GOOS
|
||||||
if Tea.IsTesting() && osName == "darwin" {
|
if Tea.IsTesting() && osName == "darwin" {
|
||||||
@@ -105,10 +108,12 @@ func (this *UpgradeManager) Start() error {
|
|||||||
}
|
}
|
||||||
url = strings.ReplaceAll(url, "${os}", osName)
|
url = strings.ReplaceAll(url, "${os}", osName)
|
||||||
url = strings.ReplaceAll(url, "${arch}", runtime.GOARCH)
|
url = strings.ReplaceAll(url, "${arch}", runtime.GOARCH)
|
||||||
|
url = strings.ReplaceAll(url, "${version}", teaconst.Version)
|
||||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("create url request failed: " + err.Error())
|
return errors.New("create url request failed: " + err.Error())
|
||||||
}
|
}
|
||||||
|
req.Header.Set("User-Agent", "Edge-Admin/"+teaconst.Version)
|
||||||
|
|
||||||
resp, err := this.client.Do(req)
|
resp, err := this.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -169,6 +174,7 @@ func (this *UpgradeManager) Start() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("create download request failed: " + err.Error())
|
return errors.New("create download request failed: " + err.Error())
|
||||||
}
|
}
|
||||||
|
req.Header.Set("User-Agent", "Edge-Admin/"+teaconst.Version)
|
||||||
|
|
||||||
resp, err := this.client.Do(req)
|
resp, err := this.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -3,12 +3,15 @@ package api
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/utils/apinodeutils"
|
||||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||||
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
|
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
|
||||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
|
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
|
||||||
"github.com/iwind/TeaGo/logs"
|
"github.com/iwind/TeaGo/logs"
|
||||||
"github.com/iwind/TeaGo/maps"
|
"github.com/iwind/TeaGo/maps"
|
||||||
|
stringutil "github.com/iwind/TeaGo/utils/string"
|
||||||
timeutil "github.com/iwind/TeaGo/utils/time"
|
timeutil "github.com/iwind/TeaGo/utils/time"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -44,7 +47,7 @@ func (this *IndexAction) RunGet(params struct{}) {
|
|||||||
|
|
||||||
for _, node := range nodesResp.ApiNodes {
|
for _, node := range nodesResp.ApiNodes {
|
||||||
// 状态
|
// 状态
|
||||||
status := &nodeconfigs.NodeStatus{}
|
var status = &nodeconfigs.NodeStatus{}
|
||||||
if len(node.StatusJSON) > 0 {
|
if len(node.StatusJSON) > 0 {
|
||||||
err = json.Unmarshal(node.StatusJSON, &status)
|
err = json.Unmarshal(node.StatusJSON, &status)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -55,7 +58,7 @@ func (this *IndexAction) RunGet(params struct{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Rest地址
|
// Rest地址
|
||||||
restAccessAddrs := []string{}
|
var restAccessAddrs = []string{}
|
||||||
if node.RestIsOn {
|
if node.RestIsOn {
|
||||||
if len(node.RestHTTPJSON) > 0 {
|
if len(node.RestHTTPJSON) > 0 {
|
||||||
httpConfig := &serverconfigs.HTTPProtocolConfig{}
|
httpConfig := &serverconfigs.HTTPProtocolConfig{}
|
||||||
@@ -86,6 +89,9 @@ func (this *IndexAction) RunGet(params struct{}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var shouldUpgrade = status.IsActive && len(status.BuildVersion) > 0 && stringutil.VersionCompare(teaconst.APINodeVersion, status.BuildVersion) > 0
|
||||||
|
canUpgrade, _ := apinodeutils.CanUpgrade(status.BuildVersion, status.OS, status.Arch)
|
||||||
|
|
||||||
nodeMaps = append(nodeMaps, maps.Map{
|
nodeMaps = append(nodeMaps, maps.Map{
|
||||||
"id": node.Id,
|
"id": node.Id,
|
||||||
"isOn": node.IsOn,
|
"isOn": node.IsOn,
|
||||||
@@ -94,14 +100,17 @@ func (this *IndexAction) RunGet(params struct{}) {
|
|||||||
"restAccessAddrs": restAccessAddrs,
|
"restAccessAddrs": restAccessAddrs,
|
||||||
"isPrimary": node.IsPrimary,
|
"isPrimary": node.IsPrimary,
|
||||||
"status": maps.Map{
|
"status": maps.Map{
|
||||||
"isActive": status.IsActive,
|
"isActive": status.IsActive,
|
||||||
"updatedAt": status.UpdatedAt,
|
"updatedAt": status.UpdatedAt,
|
||||||
"hostname": status.Hostname,
|
"hostname": status.Hostname,
|
||||||
"cpuUsage": status.CPUUsage,
|
"cpuUsage": status.CPUUsage,
|
||||||
"cpuUsageText": fmt.Sprintf("%.2f%%", status.CPUUsage*100),
|
"cpuUsageText": fmt.Sprintf("%.2f%%", status.CPUUsage*100),
|
||||||
"memUsage": status.MemoryUsage,
|
"memUsage": status.MemoryUsage,
|
||||||
"memUsageText": fmt.Sprintf("%.2f%%", status.MemoryUsage*100),
|
"memUsageText": fmt.Sprintf("%.2f%%", status.MemoryUsage*100),
|
||||||
"buildVersion": status.BuildVersion,
|
"buildVersion": status.BuildVersion,
|
||||||
|
"latestVersion": teaconst.APINodeVersion,
|
||||||
|
"shouldUpgrade": shouldUpgrade,
|
||||||
|
"canUpgrade": shouldUpgrade && canUpgrade,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ func init() {
|
|||||||
GetPost("/update", new(UpdateAction)).
|
GetPost("/update", new(UpdateAction)).
|
||||||
Get("/install", new(InstallAction)).
|
Get("/install", new(InstallAction)).
|
||||||
Get("/logs", new(LogsAction)).
|
Get("/logs", new(LogsAction)).
|
||||||
|
GetPost("/upgradePopup", new(UpgradePopupAction)).
|
||||||
|
Post("/upgradeCheck", new(UpgradeCheckAction)).
|
||||||
|
|
||||||
|
//
|
||||||
EndAll()
|
EndAll()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
67
internal/web/actions/default/api/node/upgradeCheck.go
Normal file
67
internal/web/actions/default/api/node/upgradeCheck.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||||
|
|
||||||
|
package node
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/configs"
|
||||||
|
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||||
|
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UpgradeCheckAction 检查升级结果
|
||||||
|
type UpgradeCheckAction struct {
|
||||||
|
actionutils.ParentAction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *UpgradeCheckAction) Init() {
|
||||||
|
this.Nav("", "", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *UpgradeCheckAction) RunPost(params struct {
|
||||||
|
NodeId int64
|
||||||
|
}) {
|
||||||
|
this.Data["isOk"] = false
|
||||||
|
|
||||||
|
nodeResp, err := this.RPC().APINodeRPC().FindEnabledAPINode(this.AdminContext(), &pb.FindEnabledAPINodeRequest{ApiNodeId: params.NodeId})
|
||||||
|
if err != nil {
|
||||||
|
this.Success()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var node = nodeResp.ApiNode
|
||||||
|
if node == nil || len(node.AccessAddrs) == 0 {
|
||||||
|
this.Success()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiConfig, err := configs.LoadAPIConfig()
|
||||||
|
if err != nil {
|
||||||
|
this.Success()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var newAPIConfig = apiConfig.Clone()
|
||||||
|
newAPIConfig.RPC.Endpoints = node.AccessAddrs
|
||||||
|
rpcClient, err := rpc.NewRPCClient(newAPIConfig, false)
|
||||||
|
if err != nil {
|
||||||
|
this.Success()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
versionResp, err := rpcClient.APINodeRPC().FindCurrentAPINodeVersion(rpcClient.Context(0), &pb.FindCurrentAPINodeVersionRequest{})
|
||||||
|
if err != nil {
|
||||||
|
this.Success()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if versionResp.Version != teaconst.Version {
|
||||||
|
this.Success()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Data["isOk"] = true
|
||||||
|
|
||||||
|
this.Success()
|
||||||
|
}
|
||||||
124
internal/web/actions/default/api/node/upgradePopup.go
Normal file
124
internal/web/actions/default/api/node/upgradePopup.go
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||||
|
|
||||||
|
package node
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/utils/apinodeutils"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||||
|
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
|
||||||
|
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||||
|
"github.com/iwind/TeaGo/actions"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UpgradePopupAction struct {
|
||||||
|
actionutils.ParentAction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *UpgradePopupAction) Init() {
|
||||||
|
this.Nav("", "", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *UpgradePopupAction) RunGet(params struct {
|
||||||
|
NodeId int64
|
||||||
|
}) {
|
||||||
|
this.Data["nodeId"] = params.NodeId
|
||||||
|
this.Data["nodeName"] = ""
|
||||||
|
this.Data["currentVersion"] = ""
|
||||||
|
this.Data["latestVersion"] = ""
|
||||||
|
this.Data["result"] = ""
|
||||||
|
this.Data["resultIsOk"] = true
|
||||||
|
this.Data["canUpgrade"] = false
|
||||||
|
this.Data["isUpgrading"] = false
|
||||||
|
|
||||||
|
nodeResp, err := this.RPC().APINodeRPC().FindEnabledAPINode(this.AdminContext(), &pb.FindEnabledAPINodeRequest{ApiNodeId: params.NodeId})
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var node = nodeResp.ApiNode
|
||||||
|
if node == nil {
|
||||||
|
this.Data["result"] = "要升级的节点不存在"
|
||||||
|
this.Data["resultIsOk"] = false
|
||||||
|
this.Show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.Data["nodeName"] = node.Name + " / [" + strings.Join(node.AccessAddrs, ", ") + "]"
|
||||||
|
|
||||||
|
// 节点状态
|
||||||
|
var status = &nodeconfigs.NodeStatus{}
|
||||||
|
if len(node.StatusJSON) > 0 {
|
||||||
|
err = json.Unmarshal(node.StatusJSON, &status)
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(errors.New("decode status failed: " + err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.Data["currentVersion"] = status.BuildVersion
|
||||||
|
} else {
|
||||||
|
this.Data["result"] = "无法检测到节点当前版本"
|
||||||
|
this.Data["resultIsOk"] = false
|
||||||
|
this.Show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.Data["latestVersion"] = teaconst.APINodeVersion
|
||||||
|
|
||||||
|
if status.IsActive && len(status.BuildVersion) > 0 {
|
||||||
|
canUpgrade, reason := apinodeutils.CanUpgrade(status.BuildVersion, status.OS, status.Arch)
|
||||||
|
if !canUpgrade {
|
||||||
|
this.Data["result"] = reason
|
||||||
|
this.Data["resultIsOk"] = false
|
||||||
|
this.Show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.Data["canUpgrade"] = true
|
||||||
|
this.Data["result"] = "等待升级"
|
||||||
|
this.Data["resultIsOk"] = true
|
||||||
|
} else {
|
||||||
|
this.Data["result"] = "当前节点非连接状态无法远程升级"
|
||||||
|
this.Data["resultIsOk"] = false
|
||||||
|
this.Show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 是否正在升级
|
||||||
|
var oldUpgrader = apinodeutils.SharedManager.FindUpgrader(params.NodeId)
|
||||||
|
if oldUpgrader != nil {
|
||||||
|
this.Data["result"] = "正在升级中..."
|
||||||
|
this.Data["resultIsOk"] = false
|
||||||
|
this.Data["isUpgrading"] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Show()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *UpgradePopupAction) RunPost(params struct {
|
||||||
|
NodeId int64
|
||||||
|
|
||||||
|
Must *actions.Must
|
||||||
|
CSRF *actionutils.CSRF
|
||||||
|
}) {
|
||||||
|
var manager = apinodeutils.SharedManager
|
||||||
|
|
||||||
|
var oldUpgrader = manager.FindUpgrader(params.NodeId)
|
||||||
|
if oldUpgrader != nil {
|
||||||
|
this.Fail("正在升级中,无需重复提交 ...")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var upgrader = apinodeutils.NewUpgrader(params.NodeId)
|
||||||
|
manager.AddUpgrader(upgrader)
|
||||||
|
defer func() {
|
||||||
|
manager.RemoveUpgrader(upgrader)
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := upgrader.Upgrade()
|
||||||
|
if err != nil {
|
||||||
|
this.Fail("升级失败:" + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Success()
|
||||||
|
}
|
||||||
@@ -89,7 +89,7 @@ func (this *DetailAction) RunGet(params struct {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var ipAddresses = ipAddressesResp.NodeIPAddresses
|
var ipAddresses = ipAddressesResp.NodeIPAddresses
|
||||||
ipAddressMaps := []maps.Map{}
|
var ipAddressMaps = []maps.Map{}
|
||||||
for _, addr := range ipAddressesResp.NodeIPAddresses {
|
for _, addr := range ipAddressesResp.NodeIPAddresses {
|
||||||
thresholds, err := ipaddressutils.InitNodeIPAddressThresholds(this.Parent(), addr.Id)
|
thresholds, err := ipaddressutils.InitNodeIPAddressThresholds(this.Parent(), addr.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -103,6 +103,15 @@ func (this *DetailAction) RunGet(params struct {
|
|||||||
addr.Ip = addr.BackupIP
|
addr.Ip = addr.BackupIP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 专属集群
|
||||||
|
var addrClusterMaps = []maps.Map{}
|
||||||
|
for _, addrCluster := range addr.NodeClusters {
|
||||||
|
addrClusterMaps = append(addrClusterMaps, maps.Map{
|
||||||
|
"id": addrCluster.Id,
|
||||||
|
"name": addrCluster.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
ipAddressMaps = append(ipAddressMaps, maps.Map{
|
ipAddressMaps = append(ipAddressMaps, maps.Map{
|
||||||
"id": addr.Id,
|
"id": addr.Id,
|
||||||
"name": addr.Name,
|
"name": addr.Name,
|
||||||
@@ -111,6 +120,7 @@ func (this *DetailAction) RunGet(params struct {
|
|||||||
"canAccess": addr.CanAccess,
|
"canAccess": addr.CanAccess,
|
||||||
"isOn": addr.IsOn,
|
"isOn": addr.IsOn,
|
||||||
"isUp": addr.IsUp,
|
"isUp": addr.IsUp,
|
||||||
|
"clusters": addrClusterMaps,
|
||||||
"thresholds": thresholds,
|
"thresholds": thresholds,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -152,16 +162,31 @@ func (this *DetailAction) RunGet(params struct {
|
|||||||
if !addr.CanAccess || !addr.IsUp || !addr.IsOn {
|
if !addr.CanAccess || !addr.IsUp || !addr.IsOn {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 过滤集群
|
||||||
|
if len(addr.NodeClusters) > 0 {
|
||||||
|
var inCluster = false
|
||||||
|
for _, addrCluster := range addr.NodeClusters {
|
||||||
|
if addrCluster.Id == cluster.Id {
|
||||||
|
inCluster = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !inCluster {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, route := range dnsInfo.Routes {
|
for _, route := range dnsInfo.Routes {
|
||||||
var recordType = "A"
|
var recordType = "A"
|
||||||
if utils.IsIPv6(addr.Ip) {
|
if utils.IsIPv6(addr.Ip) {
|
||||||
recordType = "AAAA"
|
recordType = "AAAA"
|
||||||
}
|
}
|
||||||
recordMaps = append(recordMaps, maps.Map{
|
recordMaps = append(recordMaps, maps.Map{
|
||||||
"name": dnsInfo.NodeClusterDNSName + "." + domainName,
|
"name": dnsInfo.NodeClusterDNSName + "." + domainName,
|
||||||
"type": recordType,
|
"type": recordType,
|
||||||
"route": route.Name,
|
"route": route.Name,
|
||||||
"value": addr.Ip,
|
"value": addr.Ip,
|
||||||
|
"clusterName": cluster.Name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,8 +204,8 @@ func (this *DetailAction) RunGet(params struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
grantMap := maps.Map{}
|
var grantMap = maps.Map{}
|
||||||
grantId := loginParams.GetInt64("grantId")
|
var grantId = loginParams.GetInt64("grantId")
|
||||||
if grantId > 0 {
|
if grantId > 0 {
|
||||||
grantResp, err := this.RPC().NodeGrantRPC().FindEnabledNodeGrant(this.AdminContext(), &pb.FindEnabledNodeGrantRequest{NodeGrantId: grantId})
|
grantResp, err := this.RPC().NodeGrantRPC().FindEnabledNodeGrant(this.AdminContext(), &pb.FindEnabledNodeGrantRequest{NodeGrantId: grantId})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -64,12 +64,22 @@ func (this *UpdateAction) RunGet(params struct {
|
|||||||
}
|
}
|
||||||
var ipAddressMaps = []maps.Map{}
|
var ipAddressMaps = []maps.Map{}
|
||||||
for _, addr := range ipAddressesResp.NodeIPAddresses {
|
for _, addr := range ipAddressesResp.NodeIPAddresses {
|
||||||
|
// 阈值
|
||||||
thresholds, err := ipaddressutils.InitNodeIPAddressThresholds(this.Parent(), addr.Id)
|
thresholds, err := ipaddressutils.InitNodeIPAddressThresholds(this.Parent(), addr.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
this.ErrorPage(err)
|
this.ErrorPage(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 专属集群
|
||||||
|
var clusterMaps = []maps.Map{}
|
||||||
|
for _, addrCluster := range addr.NodeClusters {
|
||||||
|
clusterMaps = append(clusterMaps, maps.Map{
|
||||||
|
"id": addrCluster.Id,
|
||||||
|
"name": addrCluster.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
ipAddressMaps = append(ipAddressMaps, maps.Map{
|
ipAddressMaps = append(ipAddressMaps, maps.Map{
|
||||||
"id": addr.Id,
|
"id": addr.Id,
|
||||||
"name": addr.Name,
|
"name": addr.Name,
|
||||||
@@ -78,6 +88,7 @@ func (this *UpdateAction) RunGet(params struct {
|
|||||||
"isOn": addr.IsOn,
|
"isOn": addr.IsOn,
|
||||||
"isUp": addr.IsUp,
|
"isUp": addr.IsUp,
|
||||||
"thresholds": thresholds,
|
"thresholds": thresholds,
|
||||||
|
"clusters": clusterMaps,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -142,8 +142,17 @@ func (this *NodesAction) RunGet(params struct {
|
|||||||
this.ErrorPage(err)
|
this.ErrorPage(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ipAddresses := []maps.Map{}
|
var ipAddresses = []maps.Map{}
|
||||||
for _, addr := range ipAddressesResp.NodeIPAddresses {
|
for _, addr := range ipAddressesResp.NodeIPAddresses {
|
||||||
|
// 专属集群
|
||||||
|
var addrClusterMaps = []maps.Map{}
|
||||||
|
for _, addrCluster := range addr.NodeClusters {
|
||||||
|
addrClusterMaps = append(addrClusterMaps, maps.Map{
|
||||||
|
"id": addrCluster.Id,
|
||||||
|
"name": addrCluster.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
ipAddresses = append(ipAddresses, maps.Map{
|
ipAddresses = append(ipAddresses, maps.Map{
|
||||||
"id": addr.Id,
|
"id": addr.Id,
|
||||||
"name": addr.Name,
|
"name": addr.Name,
|
||||||
@@ -151,6 +160,7 @@ func (this *NodesAction) RunGet(params struct {
|
|||||||
"canAccess": addr.CanAccess,
|
"canAccess": addr.CanAccess,
|
||||||
"isUp": addr.IsUp,
|
"isUp": addr.IsUp,
|
||||||
"isOn": addr.IsOn,
|
"isOn": addr.IsOn,
|
||||||
|
"clusters": addrClusterMaps,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ func (this *IndexAction) RunPost(params struct {
|
|||||||
HttpAccessLogEnableResponseHeaders bool
|
HttpAccessLogEnableResponseHeaders bool
|
||||||
HttpAccessLogCommonRequestHeadersOnly bool
|
HttpAccessLogCommonRequestHeadersOnly bool
|
||||||
HttpAccessLogEnableCookies bool
|
HttpAccessLogEnableCookies bool
|
||||||
|
HttpAccessLogEnableServerNotFound bool
|
||||||
|
|
||||||
LogRecordServerError bool
|
LogRecordServerError bool
|
||||||
|
|
||||||
@@ -138,6 +139,7 @@ func (this *IndexAction) RunPost(params struct {
|
|||||||
config.HTTPAccessLog.EnableResponseHeaders = params.HttpAccessLogEnableResponseHeaders
|
config.HTTPAccessLog.EnableResponseHeaders = params.HttpAccessLogEnableResponseHeaders
|
||||||
config.HTTPAccessLog.CommonRequestHeadersOnly = params.HttpAccessLogCommonRequestHeadersOnly
|
config.HTTPAccessLog.CommonRequestHeadersOnly = params.HttpAccessLogCommonRequestHeadersOnly
|
||||||
config.HTTPAccessLog.EnableCookies = params.HttpAccessLogEnableCookies
|
config.HTTPAccessLog.EnableCookies = params.HttpAccessLogEnableCookies
|
||||||
|
config.HTTPAccessLog.EnableServerNotFound = params.HttpAccessLogEnableServerNotFound
|
||||||
|
|
||||||
// 日志
|
// 日志
|
||||||
config.Log.RecordServerError = params.LogRecordServerError
|
config.Log.RecordServerError = params.LogRecordServerError
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ func (this *ClusterHelper) BeforeAction(actionPtr actions.ActionWrapper) (goNext
|
|||||||
actionutils.SetTabbar(action, tabbar)
|
actionutils.SetTabbar(action, tabbar)
|
||||||
|
|
||||||
// 左侧菜单
|
// 左侧菜单
|
||||||
secondMenuItem := action.Data.GetString("secondMenuItem")
|
var secondMenuItem = action.Data.GetString("secondMenuItem")
|
||||||
switch selectedTabbar {
|
switch selectedTabbar {
|
||||||
case "setting":
|
case "setting":
|
||||||
var menuItems = this.createSettingMenu(cluster, clusterInfo, secondMenuItem)
|
var menuItems = this.createSettingMenu(cluster, clusterInfo, secondMenuItem)
|
||||||
|
|||||||
60
internal/web/actions/default/clusters/logs/deleteAll.go
Normal file
60
internal/web/actions/default/clusters/logs/deleteAll.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||||
|
|
||||||
|
package logs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||||
|
"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
|
||||||
|
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
|
||||||
|
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DeleteAllAction struct {
|
||||||
|
actionutils.ParentAction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *DeleteAllAction) RunPost(params struct {
|
||||||
|
DayFrom string
|
||||||
|
DayTo string
|
||||||
|
Keyword string
|
||||||
|
Level string
|
||||||
|
Type string // unread, needFix
|
||||||
|
Tag string
|
||||||
|
ClusterId int64
|
||||||
|
NodeId int64
|
||||||
|
}) {
|
||||||
|
defer this.CreateLogInfo("批量删除节点运行日志")
|
||||||
|
|
||||||
|
// 目前仅允许通过关键词删除,防止误删
|
||||||
|
if len(params.Keyword) == 0 {
|
||||||
|
this.Fail("目前仅允许通过关键词删除")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var fixedState configutils.BoolState = 0
|
||||||
|
var allServers = false
|
||||||
|
if params.Type == "needFix" {
|
||||||
|
fixedState = configutils.BoolStateNo
|
||||||
|
allServers = true
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := this.RPC().NodeLogRPC().DeleteNodeLogs(this.AdminContext(), &pb.DeleteNodeLogsRequest{
|
||||||
|
NodeClusterId: params.ClusterId,
|
||||||
|
NodeId: params.NodeId,
|
||||||
|
Role: nodeconfigs.NodeRoleNode,
|
||||||
|
DayFrom: params.DayFrom,
|
||||||
|
DayTo: params.DayTo,
|
||||||
|
Keyword: params.Keyword,
|
||||||
|
Level: params.Level,
|
||||||
|
IsUnread: params.Type == "unread",
|
||||||
|
Tag: params.Tag,
|
||||||
|
FixedState: int32(fixedState),
|
||||||
|
AllServers: allServers,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Success()
|
||||||
|
}
|
||||||
@@ -39,6 +39,7 @@ func (this *IndexAction) RunGet(params struct {
|
|||||||
this.Data["dayFrom"] = params.DayFrom
|
this.Data["dayFrom"] = params.DayFrom
|
||||||
this.Data["dayTo"] = params.DayTo
|
this.Data["dayTo"] = params.DayTo
|
||||||
this.Data["keyword"] = params.Keyword
|
this.Data["keyword"] = params.Keyword
|
||||||
|
this.Data["searchedKeyword"] = params.Keyword
|
||||||
this.Data["level"] = params.Level
|
this.Data["level"] = params.Level
|
||||||
this.Data["type"] = params.Type
|
this.Data["type"] = params.Type
|
||||||
this.Data["tag"] = params.Tag
|
this.Data["tag"] = params.Tag
|
||||||
@@ -97,6 +98,8 @@ func (this *IndexAction) RunGet(params struct {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var count = countResp.Count
|
var count = countResp.Count
|
||||||
|
this.Data["countLogs"] = count
|
||||||
|
|
||||||
var page = this.NewPage(count)
|
var page = this.NewPage(count)
|
||||||
this.Data["page"] = page.AsHTML()
|
this.Data["page"] = page.AsHTML()
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ func init() {
|
|||||||
Post("/readAllLogs", new(ReadAllLogsAction)).
|
Post("/readAllLogs", new(ReadAllLogsAction)).
|
||||||
Post("/fix", new(FixAction)).
|
Post("/fix", new(FixAction)).
|
||||||
Post("/fixAll", new(FixAllAction)).
|
Post("/fixAll", new(FixAllAction)).
|
||||||
|
Post("/deleteAll", new(DeleteAllAction)).
|
||||||
EndAll()
|
EndAll()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,8 +144,17 @@ func (this *NodesAction) RunGet(params struct {
|
|||||||
this.ErrorPage(err)
|
this.ErrorPage(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ipAddresses := []maps.Map{}
|
var ipAddresses = []maps.Map{}
|
||||||
for _, addr := range ipAddressesResp.NodeIPAddresses {
|
for _, addr := range ipAddressesResp.NodeIPAddresses {
|
||||||
|
// 专属集群
|
||||||
|
var addrClusterMaps = []maps.Map{}
|
||||||
|
for _, addrCluster := range addr.NodeClusters {
|
||||||
|
addrClusterMaps = append(addrClusterMaps, maps.Map{
|
||||||
|
"id": addrCluster.Id,
|
||||||
|
"name": addrCluster.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
ipAddresses = append(ipAddresses, maps.Map{
|
ipAddresses = append(ipAddresses, maps.Map{
|
||||||
"id": addr.Id,
|
"id": addr.Id,
|
||||||
"name": addr.Name,
|
"name": addr.Name,
|
||||||
@@ -153,6 +162,7 @@ func (this *NodesAction) RunGet(params struct {
|
|||||||
"canAccess": addr.CanAccess,
|
"canAccess": addr.CanAccess,
|
||||||
"isUp": addr.IsUp,
|
"isUp": addr.IsUp,
|
||||||
"isOn": addr.IsOn,
|
"isOn": addr.IsOn,
|
||||||
|
"clusters": addrClusterMaps,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +185,7 @@ func (this *NodesAction) RunGet(params struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DNS
|
// DNS
|
||||||
dnsRouteNames := []string{}
|
var dnsRouteNames = []string{}
|
||||||
for _, route := range node.DnsRoutes {
|
for _, route := range node.DnsRoutes {
|
||||||
dnsRouteNames = append(dnsRouteNames, route.Name)
|
dnsRouteNames = append(dnsRouteNames, route.Name)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
package index
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
|
||||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
|
||||||
"github.com/iwind/TeaGo/actions"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 检查是否需要OTP
|
|
||||||
type CheckOTPAction struct {
|
|
||||||
actionutils.ParentAction
|
|
||||||
}
|
|
||||||
|
|
||||||
func (this *CheckOTPAction) Init() {
|
|
||||||
this.Nav("", "", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (this *CheckOTPAction) RunPost(params struct {
|
|
||||||
Username string
|
|
||||||
|
|
||||||
Must *actions.Must
|
|
||||||
}) {
|
|
||||||
checkResp, err := this.RPC().AdminRPC().CheckAdminOTPWithUsername(this.AdminContext(), &pb.CheckAdminOTPWithUsernameRequest{Username: params.Username})
|
|
||||||
if err != nil {
|
|
||||||
this.ErrorPage(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.Data["requireOTP"] = checkResp.RequireOTP
|
|
||||||
this.Success()
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package index
|
package index
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
|
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
|
||||||
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
|
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
|
||||||
@@ -14,10 +13,8 @@ import (
|
|||||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/dao"
|
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/dao"
|
||||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||||
"github.com/iwind/TeaGo/actions"
|
"github.com/iwind/TeaGo/actions"
|
||||||
"github.com/iwind/TeaGo/maps"
|
|
||||||
"github.com/iwind/TeaGo/types"
|
"github.com/iwind/TeaGo/types"
|
||||||
stringutil "github.com/iwind/TeaGo/utils/string"
|
stringutil "github.com/iwind/TeaGo/utils/string"
|
||||||
"github.com/xlzd/gotp"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,7 +24,8 @@ type IndexAction struct {
|
|||||||
|
|
||||||
// 首页(登录页)
|
// 首页(登录页)
|
||||||
|
|
||||||
var TokenSalt = stringutil.Rand(32)
|
// TokenKey 加密用的密钥
|
||||||
|
var TokenKey = stringutil.Rand(32)
|
||||||
|
|
||||||
func (this *IndexAction) RunGet(params struct {
|
func (this *IndexAction) RunGet(params struct {
|
||||||
From string
|
From string
|
||||||
@@ -59,7 +57,7 @@ func (this *IndexAction) RunGet(params struct {
|
|||||||
this.Data["menu"] = "signIn"
|
this.Data["menu"] = "signIn"
|
||||||
|
|
||||||
var timestamp = fmt.Sprintf("%d", time.Now().Unix())
|
var timestamp = fmt.Sprintf("%d", time.Now().Unix())
|
||||||
this.Data["token"] = stringutil.Md5(TokenSalt+timestamp) + timestamp
|
this.Data["token"] = stringutil.Md5(TokenKey+timestamp) + timestamp
|
||||||
this.Data["from"] = params.From
|
this.Data["from"] = params.From
|
||||||
|
|
||||||
uiConfig, err := configloaders.LoadAdminUIConfig()
|
uiConfig, err := configloaders.LoadAdminUIConfig()
|
||||||
@@ -93,9 +91,10 @@ func (this *IndexAction) RunPost(params struct {
|
|||||||
Password string
|
Password string
|
||||||
OtpCode string
|
OtpCode string
|
||||||
Remember bool
|
Remember bool
|
||||||
Must *actions.Must
|
|
||||||
Auth *helpers.UserShouldAuth
|
Must *actions.Must
|
||||||
CSRF *actionutils.CSRF
|
Auth *helpers.UserShouldAuth
|
||||||
|
CSRF *actionutils.CSRF
|
||||||
}) {
|
}) {
|
||||||
params.Must.
|
params.Must.
|
||||||
Field("username", params.Username).
|
Field("username", params.Username).
|
||||||
@@ -112,7 +111,7 @@ func (this *IndexAction) RunPost(params struct {
|
|||||||
this.Fail("请通过登录页面登录")
|
this.Fail("请通过登录页面登录")
|
||||||
}
|
}
|
||||||
var timestampString = params.Token[32:]
|
var timestampString = params.Token[32:]
|
||||||
if stringutil.Md5(TokenSalt+timestampString) != params.Token[:32] {
|
if stringutil.Md5(TokenKey+timestampString) != params.Token[:32] {
|
||||||
this.FailField("refresh", "登录页面已过期,请刷新后重试")
|
this.FailField("refresh", "登录页面已过期,请刷新后重试")
|
||||||
}
|
}
|
||||||
var timestamp = types.Int64(timestampString)
|
var timestamp = types.Int64(timestampString)
|
||||||
@@ -123,6 +122,7 @@ func (this *IndexAction) RunPost(params struct {
|
|||||||
rpcClient, err := rpc.SharedRPC()
|
rpcClient, err := rpc.SharedRPC()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
this.Fail("服务器出了点小问题:" + err.Error())
|
this.Fail("服务器出了点小问题:" + err.Error())
|
||||||
|
return
|
||||||
}
|
}
|
||||||
resp, err := rpcClient.AdminRPC().LoginAdmin(rpcClient.Context(0), &pb.LoginAdminRequest{
|
resp, err := rpcClient.AdminRPC().LoginAdmin(rpcClient.Context(0), &pb.LoginAdminRequest{
|
||||||
Username: params.Username,
|
Username: params.Username,
|
||||||
@@ -136,6 +136,7 @@ func (this *IndexAction) RunPost(params struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
actionutils.Fail(this, err)
|
actionutils.Fail(this, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !resp.IsOk {
|
if !resp.IsOk {
|
||||||
@@ -145,31 +146,37 @@ func (this *IndexAction) RunPost(params struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.Fail("请输入正确的用户名密码")
|
this.Fail("请输入正确的用户名密码")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
var adminId = resp.AdminId
|
||||||
|
|
||||||
// 检查OTP
|
// 检查是否支持OTP
|
||||||
otpLoginResp, err := this.RPC().LoginRPC().FindEnabledLogin(this.AdminContext(), &pb.FindEnabledLoginRequest{
|
checkOTPResp, err := this.RPC().AdminRPC().CheckAdminOTPWithUsername(this.AdminContext(), &pb.CheckAdminOTPWithUsernameRequest{Username: params.Username})
|
||||||
AdminId: resp.AdminId,
|
|
||||||
Type: "otp",
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
this.ErrorPage(err)
|
this.ErrorPage(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if otpLoginResp.Login != nil && otpLoginResp.Login.IsOn {
|
var requireOTP = checkOTPResp.RequireOTP
|
||||||
var loginParams = maps.Map{}
|
this.Data["requireOTP"] = requireOTP
|
||||||
err = json.Unmarshal(otpLoginResp.Login.ParamsJSON, &loginParams)
|
if requireOTP {
|
||||||
|
this.Data["remember"] = params.Remember
|
||||||
|
|
||||||
|
var sid = this.Session().Sid
|
||||||
|
this.Data["sid"] = sid
|
||||||
|
_, err = this.RPC().LoginSessionRPC().WriteLoginSessionValue(this.AdminContext(), &pb.WriteLoginSessionValueRequest{
|
||||||
|
Sid: sid + "_otp",
|
||||||
|
Key: "adminId",
|
||||||
|
Value: types.String(adminId),
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
this.ErrorPage(err)
|
this.ErrorPage(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
secret := loginParams.GetString("secret")
|
this.Success()
|
||||||
if gotp.NewDefaultTOTP(secret).Now() != params.OtpCode {
|
return
|
||||||
this.Fail("请输入正确的OTP动态密码")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var adminId = resp.AdminId
|
// 写入SESSION
|
||||||
params.Auth.StoreAdmin(adminId, params.Remember)
|
params.Auth.StoreAdmin(adminId, params.Remember)
|
||||||
|
|
||||||
// 记录日志
|
// 记录日志
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
func init() {
|
func init() {
|
||||||
TeaGo.BeforeStart(func(server *TeaGo.Server) {
|
TeaGo.BeforeStart(func(server *TeaGo.Server) {
|
||||||
server.
|
server.
|
||||||
Post("/checkOTP", new(CheckOTPAction)).
|
Prefix("").
|
||||||
Prefix("/").
|
GetPost("/", new(IndexAction)).
|
||||||
GetPost("", new(IndexAction)).
|
GetPost("/index/otp", new(OtpAction)).
|
||||||
EndAll()
|
EndAll()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
154
internal/web/actions/default/index/otp.go
Normal file
154
internal/web/actions/default/index/otp.go
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||||
|
|
||||||
|
package index
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
|
||||||
|
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/oplogs"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/setup"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
|
||||||
|
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/dao"
|
||||||
|
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||||
|
"github.com/iwind/TeaGo/actions"
|
||||||
|
"github.com/iwind/TeaGo/maps"
|
||||||
|
stringutil "github.com/iwind/TeaGo/utils/string"
|
||||||
|
"github.com/xlzd/gotp"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OtpAction struct {
|
||||||
|
actionutils.ParentAction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *OtpAction) Init() {
|
||||||
|
this.Nav("", "", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *OtpAction) RunGet(params struct {
|
||||||
|
From string
|
||||||
|
Sid string
|
||||||
|
Remember bool
|
||||||
|
}) {
|
||||||
|
// 检查系统是否已经配置过
|
||||||
|
if !setup.IsConfigured() {
|
||||||
|
this.RedirectURL("/setup")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//// 是否新安装
|
||||||
|
if setup.IsNewInstalled() {
|
||||||
|
this.RedirectURL("/setup/confirm")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Data["isUser"] = false
|
||||||
|
this.Data["menu"] = "signIn"
|
||||||
|
|
||||||
|
var timestamp = fmt.Sprintf("%d", time.Now().Unix())
|
||||||
|
this.Data["token"] = stringutil.Md5(TokenKey+timestamp) + timestamp
|
||||||
|
this.Data["from"] = params.From
|
||||||
|
this.Data["sid"] = params.Sid
|
||||||
|
|
||||||
|
uiConfig, err := configloaders.LoadAdminUIConfig()
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.Data["systemName"] = uiConfig.AdminSystemName
|
||||||
|
this.Data["showVersion"] = uiConfig.ShowVersion
|
||||||
|
if len(uiConfig.Version) > 0 {
|
||||||
|
this.Data["version"] = uiConfig.Version
|
||||||
|
} else {
|
||||||
|
this.Data["version"] = teaconst.Version
|
||||||
|
}
|
||||||
|
this.Data["faviconFileId"] = uiConfig.FaviconFileId
|
||||||
|
this.Data["remember"] = params.Remember
|
||||||
|
|
||||||
|
this.Show()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *OtpAction) RunPost(params struct {
|
||||||
|
Sid string
|
||||||
|
OtpCode string
|
||||||
|
Remember bool
|
||||||
|
|
||||||
|
Must *actions.Must
|
||||||
|
Auth *helpers.UserShouldAuth
|
||||||
|
}) {
|
||||||
|
if len(params.OtpCode) == 0 {
|
||||||
|
this.FailField("otpCode", "请输入正确的OTP动态密码")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var sid = params.Sid
|
||||||
|
if len(sid) == 0 || len(sid) > 64 {
|
||||||
|
this.Fail("参数错误,请重新登录(001)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sid += "_otp"
|
||||||
|
|
||||||
|
// 获取SESSION
|
||||||
|
sessionResp, err := this.RPC().LoginSessionRPC().FindLoginSession(this.AdminContext(), &pb.FindLoginSessionRequest{Sid: sid})
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var session = sessionResp.LoginSession
|
||||||
|
if session == nil || session.AdminId <= 0 {
|
||||||
|
this.Fail("参数错误,请重新登录(002)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var adminId = session.AdminId
|
||||||
|
|
||||||
|
// 检查OTP
|
||||||
|
otpLoginResp, err := this.RPC().LoginRPC().FindEnabledLogin(this.AdminContext(), &pb.FindEnabledLoginRequest{
|
||||||
|
AdminId: adminId,
|
||||||
|
Type: "otp",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if otpLoginResp.Login != nil && otpLoginResp.Login.IsOn {
|
||||||
|
var loginParams = maps.Map{}
|
||||||
|
err = json.Unmarshal(otpLoginResp.Login.ParamsJSON, &loginParams)
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var secret = loginParams.GetString("secret")
|
||||||
|
if gotp.NewDefaultTOTP(secret).Now() != params.OtpCode {
|
||||||
|
this.FailField("otpCode", "请输入正确的OTP动态密码")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入SESSION
|
||||||
|
params.Auth.StoreAdmin(adminId, params.Remember)
|
||||||
|
|
||||||
|
// 删除OTP SESSION
|
||||||
|
_, err = this.RPC().LoginSessionRPC().DeleteLoginSession(this.AdminContext(), &pb.DeleteLoginSessionRequest{Sid: sid})
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录日志
|
||||||
|
rpcClient, err := rpc.SharedRPC()
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = dao.SharedLogDAO.CreateAdminLog(rpcClient.Context(adminId), oplogs.LevelInfo, this.Request.URL.Path, "成功通过OTP验证登录系统", this.RequestRemoteIP())
|
||||||
|
if err != nil {
|
||||||
|
utils.PrintError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Success()
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
|
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
|
||||||
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
|
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
|
||||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/nodes/ipAddresses/ipaddressutils"
|
||||||
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
|
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
|
||||||
"github.com/iwind/TeaGo/actions"
|
"github.com/iwind/TeaGo/actions"
|
||||||
"github.com/iwind/TeaGo/maps"
|
"github.com/iwind/TeaGo/maps"
|
||||||
@@ -20,8 +21,18 @@ func (this *CreatePopupAction) Init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (this *CreatePopupAction) RunGet(params struct {
|
func (this *CreatePopupAction) RunGet(params struct {
|
||||||
|
NodeId int64
|
||||||
SupportThresholds bool
|
SupportThresholds bool
|
||||||
}) {
|
}) {
|
||||||
|
// 专属集群
|
||||||
|
clusterMaps, err := ipaddressutils.FindNodeClusterMapsWithNodeId(this.Parent(), params.NodeId)
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.Data["clusters"] = clusterMaps
|
||||||
|
|
||||||
|
// 阈值
|
||||||
this.Data["supportThresholds"] = params.SupportThresholds
|
this.Data["supportThresholds"] = params.SupportThresholds
|
||||||
|
|
||||||
this.Show()
|
this.Show()
|
||||||
@@ -33,6 +44,7 @@ func (this *CreatePopupAction) RunPost(params struct {
|
|||||||
Name string
|
Name string
|
||||||
IsUp bool
|
IsUp bool
|
||||||
ThresholdsJSON []byte
|
ThresholdsJSON []byte
|
||||||
|
ClusterIds []int64
|
||||||
|
|
||||||
Must *actions.Must
|
Must *actions.Must
|
||||||
}) {
|
}) {
|
||||||
@@ -57,6 +69,14 @@ func (this *CreatePopupAction) RunPost(params struct {
|
|||||||
_ = json.Unmarshal(params.ThresholdsJSON, &thresholds)
|
_ = json.Unmarshal(params.ThresholdsJSON, &thresholds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 专属集群
|
||||||
|
// 目前只考虑CDN边缘集群
|
||||||
|
clusterMaps, err := ipaddressutils.FindNodeClusterMaps(this.Parent(), params.ClusterIds)
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.Data["ipAddress"] = maps.Map{
|
this.Data["ipAddress"] = maps.Map{
|
||||||
"name": params.Name,
|
"name": params.Name,
|
||||||
"canAccess": params.CanAccess,
|
"canAccess": params.CanAccess,
|
||||||
@@ -65,6 +85,7 @@ func (this *CreatePopupAction) RunPost(params struct {
|
|||||||
"isOn": true,
|
"isOn": true,
|
||||||
"isUp": params.IsUp,
|
"isUp": params.IsUp,
|
||||||
"thresholds": thresholds,
|
"thresholds": thresholds,
|
||||||
|
"clusters": clusterMaps,
|
||||||
}
|
}
|
||||||
this.Success()
|
this.Success()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,14 +11,28 @@ import (
|
|||||||
|
|
||||||
// UpdateNodeIPAddresses 保存一组IP地址
|
// UpdateNodeIPAddresses 保存一组IP地址
|
||||||
func UpdateNodeIPAddresses(parentAction *actionutils.ParentAction, nodeId int64, role nodeconfigs.NodeRole, ipAddressesJSON []byte) error {
|
func UpdateNodeIPAddresses(parentAction *actionutils.ParentAction, nodeId int64, role nodeconfigs.NodeRole, ipAddressesJSON []byte) error {
|
||||||
addresses := []maps.Map{}
|
var addresses = []maps.Map{}
|
||||||
err := json.Unmarshal(ipAddressesJSON, &addresses)
|
err := json.Unmarshal(ipAddressesJSON, &addresses)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, addr := range addresses {
|
for _, addr := range addresses {
|
||||||
var resultAddrIds = []int64{}
|
var resultAddrIds = []int64{}
|
||||||
addrId := addr.GetInt64("id")
|
var addrId = addr.GetInt64("id")
|
||||||
|
|
||||||
|
// 专属集群
|
||||||
|
var addrClusterIds = []int64{}
|
||||||
|
var addrClusters = addr.GetSlice("clusters")
|
||||||
|
if len(addrClusters) > 0 {
|
||||||
|
for _, addrCluster := range addrClusters {
|
||||||
|
var m = maps.NewMap(addrCluster)
|
||||||
|
var clusterId = m.GetInt64("id")
|
||||||
|
if clusterId > 0 {
|
||||||
|
addrClusterIds = append(addrClusterIds, clusterId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if addrId > 0 {
|
if addrId > 0 {
|
||||||
resultAddrIds = append(resultAddrIds, addrId)
|
resultAddrIds = append(resultAddrIds, addrId)
|
||||||
|
|
||||||
@@ -36,6 +50,7 @@ func UpdateNodeIPAddresses(parentAction *actionutils.ParentAction, nodeId int64,
|
|||||||
CanAccess: addr.GetBool("canAccess"),
|
CanAccess: addr.GetBool("canAccess"),
|
||||||
IsOn: isOn,
|
IsOn: isOn,
|
||||||
IsUp: addr.GetBool("isUp"),
|
IsUp: addr.GetBool("isUp"),
|
||||||
|
ClusterIds: addrClusterIds,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -47,12 +62,13 @@ func UpdateNodeIPAddresses(parentAction *actionutils.ParentAction, nodeId int64,
|
|||||||
if len(result) == 1 {
|
if len(result) == 1 {
|
||||||
// 单个创建
|
// 单个创建
|
||||||
createResp, err := parentAction.RPC().NodeIPAddressRPC().CreateNodeIPAddress(parentAction.AdminContext(), &pb.CreateNodeIPAddressRequest{
|
createResp, err := parentAction.RPC().NodeIPAddressRPC().CreateNodeIPAddress(parentAction.AdminContext(), &pb.CreateNodeIPAddressRequest{
|
||||||
NodeId: nodeId,
|
NodeId: nodeId,
|
||||||
Role: role,
|
Role: role,
|
||||||
Name: addr.GetString("name"),
|
Name: addr.GetString("name"),
|
||||||
Ip: result[0],
|
Ip: result[0],
|
||||||
CanAccess: addr.GetBool("canAccess"),
|
CanAccess: addr.GetBool("canAccess"),
|
||||||
IsUp: addr.GetBool("isUp"),
|
IsUp: addr.GetBool("isUp"),
|
||||||
|
NodeClusterIds: addrClusterIds,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -62,13 +78,14 @@ func UpdateNodeIPAddresses(parentAction *actionutils.ParentAction, nodeId int64,
|
|||||||
} else if len(result) > 1 {
|
} else if len(result) > 1 {
|
||||||
// 批量创建
|
// 批量创建
|
||||||
createResp, err := parentAction.RPC().NodeIPAddressRPC().CreateNodeIPAddresses(parentAction.AdminContext(), &pb.CreateNodeIPAddressesRequest{
|
createResp, err := parentAction.RPC().NodeIPAddressRPC().CreateNodeIPAddresses(parentAction.AdminContext(), &pb.CreateNodeIPAddressesRequest{
|
||||||
NodeId: nodeId,
|
NodeId: nodeId,
|
||||||
Role: role,
|
Role: role,
|
||||||
Name: addr.GetString("name"),
|
Name: addr.GetString("name"),
|
||||||
IpList: result,
|
IpList: result,
|
||||||
CanAccess: addr.GetBool("canAccess"),
|
CanAccess: addr.GetBool("canAccess"),
|
||||||
IsUp: addr.GetBool("isUp"),
|
IsUp: addr.GetBool("isUp"),
|
||||||
GroupValue: ipStrings,
|
GroupValue: ipStrings,
|
||||||
|
NodeClusterIds: addrClusterIds,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -140,3 +157,53 @@ func InitNodeIPAddressThresholds(parentAction *actionutils.ParentAction, addrId
|
|||||||
}
|
}
|
||||||
return thresholds, nil
|
return thresholds, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindNodeClusterMapsWithNodeId 根据节点读取集群信息
|
||||||
|
func FindNodeClusterMapsWithNodeId(parentAction *actionutils.ParentAction, nodeId int64) ([]maps.Map, error) {
|
||||||
|
var clusterMaps = []maps.Map{}
|
||||||
|
if nodeId > 0 { // CDN边缘节点
|
||||||
|
nodeResp, err := parentAction.RPC().NodeRPC().FindEnabledNode(parentAction.AdminContext(), &pb.FindEnabledNodeRequest{NodeId: nodeId})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var node = nodeResp.Node
|
||||||
|
if node != nil {
|
||||||
|
var clusters = []*pb.NodeCluster{}
|
||||||
|
if node.NodeCluster != nil {
|
||||||
|
clusters = append(clusters, nodeResp.Node.NodeCluster)
|
||||||
|
}
|
||||||
|
if len(node.SecondaryNodeClusters) > 0 {
|
||||||
|
clusters = append(clusters, node.SecondaryNodeClusters...)
|
||||||
|
}
|
||||||
|
for _, cluster := range clusters {
|
||||||
|
clusterMaps = append(clusterMaps, maps.Map{
|
||||||
|
"id": cluster.Id,
|
||||||
|
"name": cluster.Name,
|
||||||
|
"isChecked": false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clusterMaps, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindNodeClusterMaps 根据一组集群ID读取集群信息
|
||||||
|
func FindNodeClusterMaps(parentAction *actionutils.ParentAction, clusterIds []int64) ([]maps.Map, error) {
|
||||||
|
var clusterMaps = []maps.Map{}
|
||||||
|
if len(clusterIds) > 0 {
|
||||||
|
for _, clusterId := range clusterIds {
|
||||||
|
clusterResp, err := parentAction.RPC().NodeClusterRPC().FindEnabledNodeCluster(parentAction.AdminContext(), &pb.FindEnabledNodeClusterRequest{NodeClusterId: clusterId})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var cluster = clusterResp.NodeCluster
|
||||||
|
if cluster != nil {
|
||||||
|
clusterMaps = append(clusterMaps, maps.Map{
|
||||||
|
"id": cluster.Id,
|
||||||
|
"name": cluster.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clusterMaps, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
|
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
|
||||||
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
|
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
|
||||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/nodes/ipAddresses/ipaddressutils"
|
||||||
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
|
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
|
||||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||||
"github.com/iwind/TeaGo/actions"
|
"github.com/iwind/TeaGo/actions"
|
||||||
@@ -21,9 +22,18 @@ func (this *UpdatePopupAction) Init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (this *UpdatePopupAction) RunGet(params struct {
|
func (this *UpdatePopupAction) RunGet(params struct {
|
||||||
|
NodeId int64
|
||||||
AddressId int64
|
AddressId int64
|
||||||
SupportThresholds bool
|
SupportThresholds bool
|
||||||
}) {
|
}) {
|
||||||
|
// 专属集群
|
||||||
|
clusterMaps, err := ipaddressutils.FindNodeClusterMapsWithNodeId(this.Parent(), params.NodeId)
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.Data["clusters"] = clusterMaps
|
||||||
|
|
||||||
this.Data["supportThresholds"] = params.SupportThresholds
|
this.Data["supportThresholds"] = params.SupportThresholds
|
||||||
|
|
||||||
this.Show()
|
this.Show()
|
||||||
@@ -37,6 +47,7 @@ func (this *UpdatePopupAction) RunPost(params struct {
|
|||||||
IsOn bool
|
IsOn bool
|
||||||
IsUp bool
|
IsUp bool
|
||||||
ThresholdsJSON []byte
|
ThresholdsJSON []byte
|
||||||
|
ClusterIds []int64
|
||||||
|
|
||||||
Must *actions.Must
|
Must *actions.Must
|
||||||
}) {
|
}) {
|
||||||
@@ -81,6 +92,14 @@ func (this *UpdatePopupAction) RunPost(params struct {
|
|||||||
_ = json.Unmarshal(params.ThresholdsJSON, &thresholds)
|
_ = json.Unmarshal(params.ThresholdsJSON, &thresholds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 专属集群
|
||||||
|
// 目前只考虑CDN边缘集群
|
||||||
|
clusterMaps, err := ipaddressutils.FindNodeClusterMaps(this.Parent(), params.ClusterIds)
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.Data["ipAddress"] = maps.Map{
|
this.Data["ipAddress"] = maps.Map{
|
||||||
"name": params.Name,
|
"name": params.Name,
|
||||||
"ip": params.IP,
|
"ip": params.IP,
|
||||||
@@ -89,6 +108,7 @@ func (this *UpdatePopupAction) RunPost(params struct {
|
|||||||
"isOn": params.IsOn,
|
"isOn": params.IsOn,
|
||||||
"isUp": isUp,
|
"isUp": isUp,
|
||||||
"thresholds": thresholds,
|
"thresholds": thresholds,
|
||||||
|
"clusters": clusterMaps,
|
||||||
}
|
}
|
||||||
|
|
||||||
this.Success()
|
this.Success()
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ func (this *IndexAction) RunGet(params struct {
|
|||||||
this.ErrorPage(err)
|
this.ErrorPage(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.Data["regions"] = ""
|
||||||
|
this.Data["isp"] = ""
|
||||||
if regionResp.IpRegion != nil {
|
if regionResp.IpRegion != nil {
|
||||||
var regionName = regionResp.IpRegion.Summary
|
var regionName = regionResp.IpRegion.Summary
|
||||||
|
|
||||||
@@ -39,10 +42,8 @@ func (this *IndexAction) RunGet(params struct {
|
|||||||
regionName = regionName[:index]
|
regionName = regionName[:index]
|
||||||
}
|
}
|
||||||
this.Data["regions"] = regionName
|
this.Data["regions"] = regionName
|
||||||
} else {
|
this.Data["isp"] = regionResp.IpRegion.Isp
|
||||||
this.Data["regions"] = ""
|
|
||||||
}
|
}
|
||||||
this.Data["isp"] = regionResp.IpRegion.Isp
|
|
||||||
|
|
||||||
// IP列表
|
// IP列表
|
||||||
ipListResp, err := this.RPC().IPListRPC().FindEnabledIPListContainsIP(this.AdminContext(), &pb.FindEnabledIPListContainsIPRequest{
|
ipListResp, err := this.RPC().IPListRPC().FindEnabledIPListContainsIP(this.AdminContext(), &pb.FindEnabledIPListContainsIPRequest{
|
||||||
|
|||||||
@@ -11,3 +11,7 @@ import (
|
|||||||
func filterMenuItems(serverConfig *serverconfigs.ServerConfig, menuItems []maps.Map, serverIdString string, secondMenuItem string) []maps.Map {
|
func filterMenuItems(serverConfig *serverconfigs.ServerConfig, menuItems []maps.Map, serverIdString string, secondMenuItem string) []maps.Map {
|
||||||
return menuItems
|
return menuItems
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func filterMenuItems2(serverConfig *serverconfigs.ServerConfig, menuItems []maps.Map, serverIdString string, secondMenuItem string) []maps.Map {
|
||||||
|
return menuItems
|
||||||
|
}
|
||||||
@@ -255,21 +255,7 @@ func (this *ServerHelper) createSettingsMenu(secondMenuItem string, serverIdStri
|
|||||||
"isOn": serverConfig.ReverseProxyRef != nil && serverConfig.ReverseProxyRef.IsOn,
|
"isOn": serverConfig.ReverseProxyRef != nil && serverConfig.ReverseProxyRef.IsOn,
|
||||||
})
|
})
|
||||||
|
|
||||||
if teaconst.IsPlus {
|
menuItems = filterMenuItems(serverConfig, menuItems, serverIdString, secondMenuItem)
|
||||||
menuItems = append(menuItems, maps.Map{
|
|
||||||
"name": "-",
|
|
||||||
"url": "",
|
|
||||||
"isActive": false,
|
|
||||||
})
|
|
||||||
|
|
||||||
menuItems = append(menuItems, maps.Map{
|
|
||||||
"name": "5秒盾",
|
|
||||||
"url": "/servers/server/settings/uam?serverId=" + serverIdString,
|
|
||||||
"isActive": secondMenuItem == "uam",
|
|
||||||
"isOn": serverConfig.UAM != nil && serverConfig.UAM.IsOn,
|
|
||||||
"isImportant": serverConfig.UAM != nil && serverConfig.UAM.IsOn,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
menuItems = append(menuItems, maps.Map{
|
menuItems = append(menuItems, maps.Map{
|
||||||
"name": "-",
|
"name": "-",
|
||||||
@@ -406,7 +392,7 @@ func (this *ServerHelper) createSettingsMenu(secondMenuItem string, serverIdStri
|
|||||||
"isOn": serverConfig.Web != nil && serverConfig.Web.RequestLimit != nil && serverConfig.Web.RequestLimit.IsOn,
|
"isOn": serverConfig.Web != nil && serverConfig.Web.RequestLimit != nil && serverConfig.Web.RequestLimit.IsOn,
|
||||||
})
|
})
|
||||||
|
|
||||||
menuItems = filterMenuItems(serverConfig, menuItems, serverIdString, secondMenuItem)
|
menuItems = filterMenuItems2(serverConfig, menuItems, serverIdString, secondMenuItem)
|
||||||
|
|
||||||
menuItems = append(menuItems, maps.Map{
|
menuItems = append(menuItems, maps.Map{
|
||||||
"name": "-",
|
"name": "-",
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ func (this *IndexAction) RunPost(params struct {
|
|||||||
var apiURL = teaconst.UpdatesURL
|
var apiURL = teaconst.UpdatesURL
|
||||||
apiURL = strings.ReplaceAll(apiURL, "${os}", runtime.GOOS)
|
apiURL = strings.ReplaceAll(apiURL, "${os}", runtime.GOOS)
|
||||||
apiURL = strings.ReplaceAll(apiURL, "${arch}", runtime.GOARCH)
|
apiURL = strings.ReplaceAll(apiURL, "${arch}", runtime.GOARCH)
|
||||||
|
apiURL = strings.ReplaceAll(apiURL, "${version}", teaconst.Version)
|
||||||
resp, err := http.Get(apiURL)
|
resp, err := http.Get(apiURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
this.Data["result"] = maps.Map{
|
this.Data["result"] = maps.Map{
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/iwind/TeaGo/maps"
|
"github.com/iwind/TeaGo/maps"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -63,10 +64,11 @@ func (this *DetectDBAction) RunPost(params struct{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.Data["localDB"] = maps.Map{
|
this.Data["localDB"] = maps.Map{
|
||||||
"host": localHost,
|
"host": localHost,
|
||||||
"port": localPort,
|
"port": localPort,
|
||||||
"username": localUsername,
|
"username": localUsername,
|
||||||
"password": localPassword,
|
"password": localPassword,
|
||||||
|
"canInstall": runtime.GOOS == "linux" && runtime.GOARCH == "amd64" && os.Getgid() == 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
this.Success()
|
this.Success()
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package setup
|
package setup
|
||||||
|
|
||||||
import "github.com/iwind/TeaGo"
|
import (
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/setup/mysql"
|
||||||
|
"github.com/iwind/TeaGo"
|
||||||
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
TeaGo.BeforeStart(func(server *TeaGo.Server) {
|
TeaGo.BeforeStart(func(server *TeaGo.Server) {
|
||||||
@@ -15,6 +18,8 @@ func init() {
|
|||||||
Post("/status", new(StatusAction)).
|
Post("/status", new(StatusAction)).
|
||||||
Post("/detectDB", new(DetectDBAction)).
|
Post("/detectDB", new(DetectDBAction)).
|
||||||
Post("/checkLocalIP", new(CheckLocalIPAction)).
|
Post("/checkLocalIP", new(CheckLocalIPAction)).
|
||||||
|
GetPost("/mysql/installPopup", new(mysql.InstallPopupAction)).
|
||||||
|
Post("/mysql/installLogs", new(mysql.InstallLogsAction)).
|
||||||
EndAll()
|
EndAll()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
17
internal/web/actions/default/setup/mysql/installLogs.go
Normal file
17
internal/web/actions/default/setup/mysql/installLogs.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/setup/mysql/mysqlinstallers/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InstallLogsAction struct {
|
||||||
|
actionutils.ParentAction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *InstallLogsAction) RunPost(params struct{}) {
|
||||||
|
this.Data["logs"] = utils.SharedLogger.ReadAll()
|
||||||
|
this.Success()
|
||||||
|
}
|
||||||
47
internal/web/actions/default/setup/mysql/installPopup.go
Normal file
47
internal/web/actions/default/setup/mysql/installPopup.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/setup/mysql/mysqlinstallers"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/setup/mysql/mysqlinstallers/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InstallPopupAction struct {
|
||||||
|
actionutils.ParentAction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *InstallPopupAction) RunGet(params struct{}) {
|
||||||
|
this.Show()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *InstallPopupAction) RunPost(params struct{}) {
|
||||||
|
// 清空日志
|
||||||
|
utils.SharedLogger.Reset()
|
||||||
|
|
||||||
|
this.Data["isOk"] = false
|
||||||
|
|
||||||
|
var installer = mysqlinstallers.NewMySQLInstaller()
|
||||||
|
var targetDir = "/usr/local/mysql"
|
||||||
|
xzFile, err := installer.Download()
|
||||||
|
if err != nil {
|
||||||
|
this.Data["err"] = "download failed: " + err.Error()
|
||||||
|
this.Success()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = installer.InstallFromFile(xzFile, targetDir)
|
||||||
|
if err != nil {
|
||||||
|
this.Data["err"] = "install from '" + xzFile + "' failed: " + err.Error()
|
||||||
|
this.Success()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Data["user"] = "root"
|
||||||
|
this.Data["password"] = installer.Password()
|
||||||
|
this.Data["dir"] = targetDir
|
||||||
|
this.Data["isOk"] = true
|
||||||
|
|
||||||
|
this.Success()
|
||||||
|
}
|
||||||
@@ -0,0 +1,612 @@
|
|||||||
|
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||||
|
|
||||||
|
package mysqlinstallers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/setup/mysql/mysqlinstallers/utils"
|
||||||
|
timeutil "github.com/iwind/TeaGo/utils/time"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MySQLInstaller struct {
|
||||||
|
password string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMySQLInstaller() *MySQLInstaller {
|
||||||
|
return &MySQLInstaller{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *MySQLInstaller) InstallFromFile(xzFilePath string, targetDir string) error {
|
||||||
|
// check whether mysql already running
|
||||||
|
this.log("checking mysqld ...")
|
||||||
|
var oldPid = utils.FindPidWithName("mysqld")
|
||||||
|
if oldPid > 0 {
|
||||||
|
return errors.New("there is already a running mysql server process, pid: '" + strconv.Itoa(oldPid) + "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check target dir
|
||||||
|
this.log("checking target dir '" + targetDir + "' ...")
|
||||||
|
_, err := os.Stat(targetDir)
|
||||||
|
if err == nil {
|
||||||
|
// check target dir
|
||||||
|
matches, _ := filepath.Glob(targetDir + "/*")
|
||||||
|
if len(matches) > 0 {
|
||||||
|
return errors.New("target dir '" + targetDir + "' already exists and not empty")
|
||||||
|
} else {
|
||||||
|
err = os.Remove(targetDir)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("clean target dir '" + targetDir + "' failed: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check 'tar' command
|
||||||
|
this.log("checking 'tar' command ...")
|
||||||
|
var tarExe, _ = exec.LookPath("tar")
|
||||||
|
if len(tarExe) == 0 {
|
||||||
|
this.log("installing 'tar' command ...")
|
||||||
|
err = this.installTarCommand()
|
||||||
|
if err != nil {
|
||||||
|
this.log("WARN: failed to install 'tar' ...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check commands
|
||||||
|
this.log("checking system commands ...")
|
||||||
|
var cmdList = []string{"tar" /** again **/, "chown", "sh"}
|
||||||
|
for _, cmd := range cmdList {
|
||||||
|
cmdPath, err := exec.LookPath(cmd)
|
||||||
|
if err != nil || len(cmdPath) == 0 {
|
||||||
|
return errors.New("could not find '" + cmd + "' command in this system")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
groupAddExe, err := this.lookupGroupAdd()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("could not find 'groupadd' command in this system")
|
||||||
|
}
|
||||||
|
|
||||||
|
userAddExe, err := this.lookupUserAdd()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("could not find 'useradd' command in this system")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ubuntu apt
|
||||||
|
aptExe, err := exec.LookPath("apt")
|
||||||
|
if err == nil && len(aptExe) > 0 {
|
||||||
|
for _, lib := range []string{"libaio1", "libncurses5"} {
|
||||||
|
this.log("checking " + lib + " ...")
|
||||||
|
var cmd = utils.NewCmd("apt", "-y", "install", lib)
|
||||||
|
cmd.WithStderr()
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("install " + lib + " failed: " + cmd.Stderr())
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
} else { // yum
|
||||||
|
yumExe, err := exec.LookPath("yum")
|
||||||
|
if err == nil && len(yumExe) > 0 {
|
||||||
|
for _, lib := range []string{"libaio", "ncurses-libs", "ncurses-compat-libs"} {
|
||||||
|
var cmd = utils.NewCmd("yum", "-y", "install", lib)
|
||||||
|
_ = cmd.Run()
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create 'mysql' user group
|
||||||
|
this.log("checking 'mysql' user group ...")
|
||||||
|
{
|
||||||
|
data, err := os.ReadFile("/etc/group")
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("check user group failed: " + err.Error())
|
||||||
|
}
|
||||||
|
if !bytes.Contains(data, []byte("\nmysql:")) {
|
||||||
|
var cmd = utils.NewCmd(groupAddExe, "mysql")
|
||||||
|
cmd.WithStderr()
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("add 'mysql' user group failed: " + cmd.Stderr())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create 'mysql' user
|
||||||
|
this.log("checking 'mysql' user ...")
|
||||||
|
{
|
||||||
|
data, err := os.ReadFile("/etc/passwd")
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("check user failed: " + err.Error())
|
||||||
|
}
|
||||||
|
if !bytes.Contains(data, []byte("\nmysql:")) {
|
||||||
|
var cmd *utils.Cmd
|
||||||
|
if strings.HasSuffix(userAddExe, "useradd") {
|
||||||
|
cmd = utils.NewCmd(userAddExe, "mysql", "-g", "mysql")
|
||||||
|
} else { // adduser
|
||||||
|
cmd = utils.NewCmd(userAddExe, "-S", "-G", "mysql", "mysql")
|
||||||
|
}
|
||||||
|
cmd.WithStderr()
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("add 'mysql' user failed: " + cmd.Stderr())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mkdir
|
||||||
|
{
|
||||||
|
var parentDir = filepath.Dir(targetDir)
|
||||||
|
stat, err := os.Stat(parentDir)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
err = os.MkdirAll(parentDir, 0777)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("try to create dir '" + parentDir + "' failed: " + err.Error())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return errors.New("check dir '" + parentDir + "' failed: " + err.Error())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !stat.IsDir() {
|
||||||
|
return errors.New("'" + parentDir + "' should be a directory")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check installer file .xz
|
||||||
|
this.log("checking installer file ...")
|
||||||
|
{
|
||||||
|
stat, err := os.Stat(xzFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("could not open the installer file: " + err.Error())
|
||||||
|
}
|
||||||
|
if stat.IsDir() {
|
||||||
|
return errors.New("'" + xzFilePath + "' not a valid file")
|
||||||
|
}
|
||||||
|
|
||||||
|
var basename = filepath.Base(xzFilePath)
|
||||||
|
if !strings.HasSuffix(basename, ".xz") {
|
||||||
|
return errors.New("installer file should has '.xz' extension")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract
|
||||||
|
this.log("extracting installer file ...")
|
||||||
|
var tmpDir = os.TempDir() + "/goedge-mysql-tmp"
|
||||||
|
{
|
||||||
|
_, err := os.Stat(tmpDir)
|
||||||
|
if err == nil {
|
||||||
|
err = os.RemoveAll(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("clean temporary directory '" + tmpDir + "' failed: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = os.Mkdir(tmpDir, 0777)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("create temporary directory '" + tmpDir + "' failed: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var cmd = utils.NewCmd("tar", "-xJvf", xzFilePath, "-C", tmpDir)
|
||||||
|
cmd.WithStderr()
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("extract installer file '" + xzFilePath + "' failed: " + cmd.Stderr())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create datadir
|
||||||
|
matches, err := filepath.Glob(tmpDir + "/mysql-*")
|
||||||
|
if err != nil || len(matches) == 0 {
|
||||||
|
return errors.New("could not find mysql installer directory from '" + tmpDir + "'")
|
||||||
|
}
|
||||||
|
var baseDir = matches[0]
|
||||||
|
var dataDir = baseDir + "/data"
|
||||||
|
_, err = os.Stat(dataDir)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
err = os.Mkdir(dataDir, 0777)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("create data dir '" + dataDir + "' failed: " + err.Error())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return errors.New("check data dir '" + dataDir + "' failed: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// chown datadir
|
||||||
|
{
|
||||||
|
var cmd = utils.NewCmd("chown", "mysql:mysql", dataDir)
|
||||||
|
cmd.WithStderr()
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("chown data dir '" + dataDir + "' failed: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create my.cnf
|
||||||
|
var myCnfFile = "/etc/my.cnf"
|
||||||
|
_, err = os.Stat(myCnfFile)
|
||||||
|
if err == nil {
|
||||||
|
// backup it
|
||||||
|
err = os.Rename(myCnfFile, "/etc/my.cnf."+timeutil.Format("YmdHis"))
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("backup '/etc/my.cnf' failed: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mysql server options https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html
|
||||||
|
var myCnfTemplate = this.createMyCnf(baseDir, dataDir)
|
||||||
|
err = os.WriteFile(myCnfFile, []byte(myCnfTemplate), 0666)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("write '" + myCnfFile + "' failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize
|
||||||
|
this.log("initializing mysql ...")
|
||||||
|
var generatedPassword = ""
|
||||||
|
{
|
||||||
|
var cmd = utils.NewCmd(baseDir+"/bin/mysqld", "--initialize", "--user=mysql")
|
||||||
|
cmd.WithStderr()
|
||||||
|
cmd.WithStdout()
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("initialize failed: " + cmd.Stderr())
|
||||||
|
}
|
||||||
|
|
||||||
|
// read from stdout
|
||||||
|
var match = regexp.MustCompile(`temporary password is.+:\s*(.+)`).FindStringSubmatch(cmd.Stdout())
|
||||||
|
if len(match) == 0 {
|
||||||
|
// read from stderr
|
||||||
|
match = regexp.MustCompile(`temporary password is.+:\s*(.+)`).FindStringSubmatch(cmd.Stderr())
|
||||||
|
|
||||||
|
if len(match) == 0 {
|
||||||
|
return errors.New("initialize successfully, but could not find generated password, please report to developer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
generatedPassword = strings.TrimSpace(match[1])
|
||||||
|
|
||||||
|
// write password to file
|
||||||
|
var passwordFile = baseDir + "/generated-password.txt"
|
||||||
|
err = os.WriteFile(passwordFile, []byte(generatedPassword), 0666)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("write password failed: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// move to right place
|
||||||
|
this.log("moving files to target dir ...")
|
||||||
|
err = os.Rename(baseDir, targetDir)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("move '" + baseDir + "' to '" + targetDir + "' failed: " + err.Error())
|
||||||
|
}
|
||||||
|
baseDir = targetDir
|
||||||
|
|
||||||
|
// change my.cnf
|
||||||
|
myCnfTemplate = this.createMyCnf(baseDir, baseDir+"/data")
|
||||||
|
err = os.WriteFile(myCnfFile, []byte(myCnfTemplate), 0666)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("create new '" + myCnfFile + "' failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// start mysql
|
||||||
|
this.log("starting mysql ...")
|
||||||
|
{
|
||||||
|
var cmd = utils.NewCmd(baseDir+"/bin/mysqld_safe", "--user=mysql")
|
||||||
|
cmd.WithStderr()
|
||||||
|
err = cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("start failed '" + cmd.String() + "': " + cmd.Stderr())
|
||||||
|
}
|
||||||
|
|
||||||
|
// waiting for startup
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
_, err = net.Dial("tcp", "127.0.0.1:3306")
|
||||||
|
if err != nil {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
// change password
|
||||||
|
newPassword, err := this.generatePassword()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("generate new password failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log("changing mysql password ...")
|
||||||
|
var passwordSQL = "ALTER USER 'root'@'localhost' IDENTIFIED BY '" + newPassword + "';"
|
||||||
|
{
|
||||||
|
var cmd = utils.NewCmd("sh", "-c", baseDir+"/bin/mysql --user=root --password=\""+generatedPassword+"\" --execute=\""+passwordSQL+"\" --connect-expired-password")
|
||||||
|
cmd.WithStderr()
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("change password failed: " + cmd.String() + ": " + cmd.Stderr())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.password = newPassword
|
||||||
|
var passwordFile = baseDir + "/generated-password.txt"
|
||||||
|
err = os.WriteFile(passwordFile, []byte(this.password), 0666)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("write generated file failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove temporary directory
|
||||||
|
_ = os.Remove(tmpDir)
|
||||||
|
|
||||||
|
// create link to 'mysql' client command
|
||||||
|
var clientExe = "/usr/local/bin/mysql"
|
||||||
|
_, err = os.Stat(clientExe)
|
||||||
|
if err != nil && os.IsNotExist(err) {
|
||||||
|
err = os.Symlink(baseDir+"/bin/mysql", clientExe)
|
||||||
|
if err == nil {
|
||||||
|
this.log("created symbolic link '" + clientExe + "' to '" + baseDir + "/bin/mysql'")
|
||||||
|
} else {
|
||||||
|
this.log("WARN: failed to create symbolic link '" + clientExe + "' to '" + baseDir + "/bin/mysql': " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// install service
|
||||||
|
// this is not required, so we ignore all errors
|
||||||
|
err = this.installService(baseDir)
|
||||||
|
if err != nil {
|
||||||
|
this.log("WARN: install service failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log("finished")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *MySQLInstaller) Download() (path string, err error) {
|
||||||
|
var client = &http.Client{}
|
||||||
|
|
||||||
|
// check latest version
|
||||||
|
this.log("checking mysql latest version ...")
|
||||||
|
var latestVersion = "8.0.31" // 默认版本
|
||||||
|
{
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "https://dev.mysql.com/downloads/mysql/", nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("User-Agent", "curl/7.61.1")
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("check latest version failed: " + err.Error())
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
data, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("read latest version failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
var reg = regexp.MustCompile(`<h1>MySQL Community Server ([\d.]+) </h1>`)
|
||||||
|
var matches = reg.FindSubmatch(data)
|
||||||
|
if len(matches) > 0 {
|
||||||
|
latestVersion = string(matches[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.log("found version: v" + latestVersion)
|
||||||
|
|
||||||
|
// download
|
||||||
|
this.log("start downloading ...")
|
||||||
|
var downloadURL = "https://cdn.mysql.com/Downloads/MySQL-8.0/mysql-" + latestVersion + "-linux-glibc2.17-x86_64-minimal.tar.xz"
|
||||||
|
|
||||||
|
{
|
||||||
|
req, err := http.NewRequest(http.MethodGet, downloadURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36")
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("check latest version failed: " + err.Error())
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", errors.New("check latest version failed: invalid response code: " + strconv.Itoa(resp.StatusCode))
|
||||||
|
}
|
||||||
|
|
||||||
|
path = filepath.Base(downloadURL)
|
||||||
|
fp, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("create download file '" + path + "' failed: " + err.Error())
|
||||||
|
}
|
||||||
|
var writer = utils.NewProgressWriter(fp, resp.ContentLength)
|
||||||
|
var ticker = time.NewTicker(1 * time.Second)
|
||||||
|
var done = make(chan bool, 1)
|
||||||
|
go func() {
|
||||||
|
var lastProgress float32 = -1
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
var progress = writer.Progress()
|
||||||
|
if lastProgress < 0 || progress-lastProgress > 0.1 || progress == 1 {
|
||||||
|
lastProgress = progress
|
||||||
|
this.log(fmt.Sprintf("%.2f%%", progress*100))
|
||||||
|
}
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
_, err = io.Copy(writer, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
_ = fp.Close()
|
||||||
|
done <- true
|
||||||
|
return "", errors.New("download failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = fp.Close()
|
||||||
|
if err != nil {
|
||||||
|
done <- true
|
||||||
|
return "", errors.New("download failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second) // waiting for progress printing
|
||||||
|
done <- true
|
||||||
|
}
|
||||||
|
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password get generated password
|
||||||
|
func (this *MySQLInstaller) Password() string {
|
||||||
|
return this.password
|
||||||
|
}
|
||||||
|
|
||||||
|
// create my.cnf content
|
||||||
|
func (this *MySQLInstaller) createMyCnf(baseDir string, dataDir string) string {
|
||||||
|
return `
|
||||||
|
[mysqld]
|
||||||
|
port=3306
|
||||||
|
basedir="` + baseDir + `"
|
||||||
|
datadir="` + dataDir + `"
|
||||||
|
|
||||||
|
max_connections=256
|
||||||
|
innodb_flush_log_at_trx_commit=2
|
||||||
|
max_prepared_stmt_count=65535
|
||||||
|
binlog_cache_size=1M
|
||||||
|
binlog_stmt_cache_size=1M
|
||||||
|
thread_cache_size=32
|
||||||
|
binlog_expire_logs_seconds=604800
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate random password
|
||||||
|
func (this *MySQLInstaller) generatePassword() (string, error) {
|
||||||
|
var p = make([]byte, 16)
|
||||||
|
n, err := rand.Read(p)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%x", p[:n]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// print log
|
||||||
|
func (this *MySQLInstaller) log(message string) {
|
||||||
|
utils.SharedLogger.Push("[" + timeutil.Format("H:i:s") + "]" + message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy file
|
||||||
|
func (this *MySQLInstaller) installService(baseDir string) error {
|
||||||
|
_, err := exec.LookPath("systemctl")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log("registering systemd service ...")
|
||||||
|
|
||||||
|
var desc = `### BEGIN INIT INFO
|
||||||
|
# Provides: mysql
|
||||||
|
# Required-Start: $local_fs $network $remote_fs
|
||||||
|
# Should-Start: ypbind nscd ldap ntpd xntpd
|
||||||
|
# Required-Stop: $local_fs $network $remote_fs
|
||||||
|
# Default-Start: 2 3 4 5
|
||||||
|
# Default-Stop: 0 1 6
|
||||||
|
# Short-Description: start and stop MySQL
|
||||||
|
# Description: MySQL is a very fast and reliable SQL database engine.
|
||||||
|
### END INIT INFO
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=MySQL Service
|
||||||
|
Before=shutdown.target
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
Restart=always
|
||||||
|
RestartSec=1s
|
||||||
|
ExecStart=${BASE_DIR}/support-files/mysql.server start
|
||||||
|
ExecStop=${BASE_DIR}/support-files/mysql.server stop
|
||||||
|
ExecRestart=${BASE_DIR}/support-files/mysql.server restart
|
||||||
|
ExecStatus=${BASE_DIR}/support-files/mysql.server status
|
||||||
|
ExecReload=${BASE_DIR}/support-files/mysql.server reload
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target`
|
||||||
|
|
||||||
|
desc = strings.ReplaceAll(desc, "${BASE_DIR}", baseDir)
|
||||||
|
|
||||||
|
err = os.WriteFile("/etc/systemd/system/mysqld.service", []byte(desc), 0666)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd = utils.NewTimeoutCmd(5*time.Second, "systemctl", "enable", "mysqld.service")
|
||||||
|
cmd.WithStderr()
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("enable mysqld.service failed: " + cmd.Stderr())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// install 'tar' command automatically
|
||||||
|
func (this *MySQLInstaller) installTarCommand() error {
|
||||||
|
// yum
|
||||||
|
yumExe, err := exec.LookPath("yum")
|
||||||
|
if err == nil && len(yumExe) > 0 {
|
||||||
|
var cmd = utils.NewTimeoutCmd(10*time.Second, yumExe, "-y", "install", "tar")
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// apt
|
||||||
|
aptExe, err := exec.LookPath("apt")
|
||||||
|
if err == nil && len(aptExe) > 0 {
|
||||||
|
var cmd = utils.NewTimeoutCmd(10*time.Second, aptExe, "-y", "install", "tar")
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *MySQLInstaller) lookupGroupAdd() (string, error) {
|
||||||
|
for _, cmd := range []string{"groupadd", "addgroup"} {
|
||||||
|
path, err := exec.LookPath(cmd)
|
||||||
|
if err == nil && len(path) > 0 {
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", errors.New("not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *MySQLInstaller) lookupUserAdd() (string, error) {
|
||||||
|
for _, cmd := range []string{"useradd", "adduser"} {
|
||||||
|
path, err := exec.LookPath(cmd)
|
||||||
|
if err == nil && len(path) > 0 {
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", errors.New("not found")
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||||
|
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cmd struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
env []string
|
||||||
|
dir string
|
||||||
|
|
||||||
|
ctx context.Context
|
||||||
|
timeout time.Duration
|
||||||
|
cancelFunc func()
|
||||||
|
|
||||||
|
captureStdout bool
|
||||||
|
captureStderr bool
|
||||||
|
|
||||||
|
stdout *bytes.Buffer
|
||||||
|
stderr *bytes.Buffer
|
||||||
|
|
||||||
|
rawCmd *exec.Cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCmd(name string, args ...string) *Cmd {
|
||||||
|
return &Cmd{
|
||||||
|
name: name,
|
||||||
|
args: args,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTimeoutCmd(timeout time.Duration, name string, args ...string) *Cmd {
|
||||||
|
return (&Cmd{
|
||||||
|
name: name,
|
||||||
|
args: args,
|
||||||
|
}).WithTimeout(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Cmd) WithTimeout(timeout time.Duration) *Cmd {
|
||||||
|
this.timeout = timeout
|
||||||
|
|
||||||
|
ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
|
||||||
|
this.ctx = ctx
|
||||||
|
this.cancelFunc = cancelFunc
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Cmd) WithStdout() *Cmd {
|
||||||
|
this.captureStdout = true
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Cmd) WithStderr() *Cmd {
|
||||||
|
this.captureStderr = true
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Cmd) WithEnv(env []string) *Cmd {
|
||||||
|
this.env = env
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Cmd) WithDir(dir string) *Cmd {
|
||||||
|
this.dir = dir
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Cmd) Start() error {
|
||||||
|
var cmd = this.compose()
|
||||||
|
return cmd.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Cmd) Wait() error {
|
||||||
|
var cmd = this.compose()
|
||||||
|
return cmd.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Cmd) Run() error {
|
||||||
|
if this.cancelFunc != nil {
|
||||||
|
defer this.cancelFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd = this.compose()
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Cmd) RawStdout() string {
|
||||||
|
if this.stdout != nil {
|
||||||
|
return this.stdout.String()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Cmd) Stdout() string {
|
||||||
|
return strings.TrimSpace(this.RawStdout())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Cmd) RawStderr() string {
|
||||||
|
if this.stderr != nil {
|
||||||
|
return this.stderr.String()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Cmd) Stderr() string {
|
||||||
|
return strings.TrimSpace(this.RawStderr())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Cmd) String() string {
|
||||||
|
if this.rawCmd != nil {
|
||||||
|
return this.rawCmd.String()
|
||||||
|
}
|
||||||
|
var newCmd = exec.Command(this.name, this.args...)
|
||||||
|
return newCmd.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Cmd) Process() *os.Process {
|
||||||
|
if this.rawCmd != nil {
|
||||||
|
return this.rawCmd.Process
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Cmd) compose() *exec.Cmd {
|
||||||
|
if this.rawCmd != nil {
|
||||||
|
return this.rawCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
if this.ctx != nil {
|
||||||
|
this.rawCmd = exec.CommandContext(this.ctx, this.name, this.args...)
|
||||||
|
} else {
|
||||||
|
this.rawCmd = exec.Command(this.name, this.args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if this.env != nil {
|
||||||
|
this.rawCmd.Env = this.env
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(this.dir) > 0 {
|
||||||
|
this.rawCmd.Dir = this.dir
|
||||||
|
}
|
||||||
|
|
||||||
|
if this.captureStdout {
|
||||||
|
this.stdout = &bytes.Buffer{}
|
||||||
|
this.rawCmd.Stdout = this.stdout
|
||||||
|
}
|
||||||
|
if this.captureStderr {
|
||||||
|
this.stderr = &bytes.Buffer{}
|
||||||
|
this.rawCmd.Stderr = this.stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.rawCmd
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||||
|
|
||||||
|
package utils
|
||||||
|
|
||||||
|
var SharedLogger = NewLogger()
|
||||||
|
|
||||||
|
type Logger struct {
|
||||||
|
c chan string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogger() *Logger {
|
||||||
|
return &Logger{
|
||||||
|
c: make(chan string, 1024),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Logger) Push(msg string) {
|
||||||
|
select {
|
||||||
|
case this.c <- msg:
|
||||||
|
default:
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Logger) ReadAll() (msgList []string) {
|
||||||
|
msgList = []string{}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case msg := <-this.c:
|
||||||
|
msgList = append(msgList, msg)
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Logger) Reset() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-this.c:
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||||
|
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProcDir = "/proc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FindPidWithName(name string) int {
|
||||||
|
// process name
|
||||||
|
commFiles, err := filepath.Glob(ProcDir + "/*/comm")
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, commFile := range commFiles {
|
||||||
|
data, err := os.ReadFile(commFile)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(string(data)) == name {
|
||||||
|
var pieces = strings.Split(commFile, "/")
|
||||||
|
var pid = pieces[len(pieces)-2]
|
||||||
|
pidInt, _ := strconv.Atoi(pid)
|
||||||
|
return pidInt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||||
|
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
type ProgressWriter struct {
|
||||||
|
rawWriter io.Writer
|
||||||
|
total int64
|
||||||
|
written int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProgressWriter(rawWriter io.Writer, total int64) *ProgressWriter {
|
||||||
|
return &ProgressWriter{
|
||||||
|
rawWriter: rawWriter,
|
||||||
|
total: total,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *ProgressWriter) Write(p []byte) (n int, err error) {
|
||||||
|
n, err = this.rawWriter.Write(p)
|
||||||
|
this.written += int64(n)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *ProgressWriter) Progress() float32 {
|
||||||
|
if this.total <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return float32(float64(this.written) / float64(this.total))
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/iwind/TeaGo/maps"
|
"github.com/iwind/TeaGo/maps"
|
||||||
"github.com/iwind/TeaGo/types"
|
"github.com/iwind/TeaGo/types"
|
||||||
"net"
|
"net"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -61,7 +62,24 @@ func (this *ValidateApiAction) RunPost(params struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if net.ParseIP(params.NewHost) == nil {
|
if net.ParseIP(params.NewHost) == nil {
|
||||||
this.FailField("newHost", "请输入正确的API节点主机地址")
|
// 检查是否为域名
|
||||||
|
var domainReg = regexp.MustCompile(`^[\w-]+$`) // 这里暂不支持 unicode 域名
|
||||||
|
var digitReg = regexp.MustCompile(`^\d+$`)
|
||||||
|
var isIP = true
|
||||||
|
for _, piece := range strings.Split(params.NewHost, ".") {
|
||||||
|
if !domainReg.MatchString(piece) {
|
||||||
|
this.FailField("newHost", "请输入正确的API节点主机地址")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !digitReg.MatchString(piece) {
|
||||||
|
isIP = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isIP {
|
||||||
|
this.FailField("newHost", "请输入正确的API节点主机地址")
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
params.Must.
|
params.Must.
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ func (this *UpdateAction) RunGet(params struct {
|
|||||||
"mobile": user.Mobile,
|
"mobile": user.Mobile,
|
||||||
"isOn": user.IsOn,
|
"isOn": user.IsOn,
|
||||||
"countAccessKeys": countAccessKeys,
|
"countAccessKeys": countAccessKeys,
|
||||||
|
"bandwidthAlgo": user.BandwidthAlgo,
|
||||||
|
|
||||||
// 实名认证
|
// 实名认证
|
||||||
"hasNewIndividualIdentity": hasNewIndividualIdentity,
|
"hasNewIndividualIdentity": hasNewIndividualIdentity,
|
||||||
@@ -87,17 +88,18 @@ func (this *UpdateAction) RunGet(params struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (this *UpdateAction) RunPost(params struct {
|
func (this *UpdateAction) RunPost(params struct {
|
||||||
UserId int64
|
UserId int64
|
||||||
Username string
|
Username string
|
||||||
Pass1 string
|
Pass1 string
|
||||||
Pass2 string
|
Pass2 string
|
||||||
Fullname string
|
Fullname string
|
||||||
Mobile string
|
Mobile string
|
||||||
Tel string
|
Tel string
|
||||||
Email string
|
Email string
|
||||||
Remark string
|
Remark string
|
||||||
IsOn bool
|
IsOn bool
|
||||||
ClusterId int64
|
ClusterId int64
|
||||||
|
BandwidthAlgo string
|
||||||
|
|
||||||
// OTP
|
// OTP
|
||||||
OtpOn bool
|
OtpOn bool
|
||||||
@@ -159,6 +161,7 @@ func (this *UpdateAction) RunPost(params struct {
|
|||||||
Remark: params.Remark,
|
Remark: params.Remark,
|
||||||
IsOn: params.IsOn,
|
IsOn: params.IsOn,
|
||||||
NodeClusterId: params.ClusterId,
|
NodeClusterId: params.ClusterId,
|
||||||
|
BandwidthAlgo: params.BandwidthAlgo,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
this.ErrorPage(err)
|
this.ErrorPage(err)
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ func (this *UserAction) RunGet(params struct {
|
|||||||
"isVerified": user.IsVerified,
|
"isVerified": user.IsVerified,
|
||||||
"registeredIP": user.RegisteredIP,
|
"registeredIP": user.RegisteredIP,
|
||||||
"registeredRegion": registeredRegion,
|
"registeredRegion": registeredRegion,
|
||||||
|
"bandwidthAlgo": user.BandwidthAlgo,
|
||||||
|
|
||||||
// 实名认证
|
// 实名认证
|
||||||
"hasNewIndividualIdentity": hasNewIndividualIdentity,
|
"hasNewIndividualIdentity": hasNewIndividualIdentity,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
27
web/public/js/components/common/js-page.js
Normal file
27
web/public/js/components/common/js-page.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
Vue.component("js-page", {
|
||||||
|
props: ["v-max"],
|
||||||
|
data: function () {
|
||||||
|
let max = this.vMax
|
||||||
|
if (max == null) {
|
||||||
|
max = 0
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
max: max,
|
||||||
|
page: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateMax: function (max) {
|
||||||
|
this.max = max
|
||||||
|
},
|
||||||
|
selectPage: function(page) {
|
||||||
|
this.page = page
|
||||||
|
this.$emit("change", page)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template:`<div>
|
||||||
|
<div class="page" v-if="max > 1">
|
||||||
|
<a href="" v-for="i in max" :class="{active: i == page}" @click.prevent="selectPage(i)">{{i}}</a>
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
})
|
||||||
108
web/public/js/components/common/url-patterns-box.js
Normal file
108
web/public/js/components/common/url-patterns-box.js
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
Vue.component("url-patterns-box", {
|
||||||
|
props: ["value"],
|
||||||
|
data: function () {
|
||||||
|
let patterns = []
|
||||||
|
if (this.value != null) {
|
||||||
|
patterns = this.value
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
patterns: patterns,
|
||||||
|
isAdding: false,
|
||||||
|
|
||||||
|
addingPattern: {"type": "wildcard", "pattern": ""},
|
||||||
|
editingIndex: -1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
add: function () {
|
||||||
|
this.isAdding = true
|
||||||
|
let that = this
|
||||||
|
setTimeout(function () {
|
||||||
|
that.$refs.patternInput.focus()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
edit: function (index) {
|
||||||
|
this.isAdding = true
|
||||||
|
this.editingIndex = index
|
||||||
|
this.addingPattern = {
|
||||||
|
type: this.patterns[index].type,
|
||||||
|
pattern: this.patterns[index].pattern
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirm: function () {
|
||||||
|
let pattern = this.addingPattern.pattern.trim()
|
||||||
|
if (pattern.length == 0) {
|
||||||
|
let that = this
|
||||||
|
teaweb.warn("请输入URL", function () {
|
||||||
|
that.$refs.patternInput.focus()
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.editingIndex < 0) {
|
||||||
|
this.patterns.push({
|
||||||
|
type: this.addingPattern.type,
|
||||||
|
pattern: this.addingPattern.pattern
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.patterns[this.editingIndex].type = this.addingPattern.type
|
||||||
|
this.patterns[this.editingIndex].pattern = this.addingPattern.pattern
|
||||||
|
}
|
||||||
|
this.notifyChange()
|
||||||
|
this.cancel()
|
||||||
|
},
|
||||||
|
remove: function (index) {
|
||||||
|
this.patterns.$remove(index)
|
||||||
|
this.cancel()
|
||||||
|
this.notifyChange()
|
||||||
|
},
|
||||||
|
cancel: function () {
|
||||||
|
this.isAdding = false
|
||||||
|
this.addingPattern = {"type": "wildcard", "pattern": ""}
|
||||||
|
this.editingIndex = -1
|
||||||
|
},
|
||||||
|
patternTypeName: function (patternType) {
|
||||||
|
switch (patternType) {
|
||||||
|
case "wildcard":
|
||||||
|
return "通配符"
|
||||||
|
case "regexp":
|
||||||
|
return "正则"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
notifyChange: function () {
|
||||||
|
this.$emit("input", this.patterns)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `<div>
|
||||||
|
<div v-show="patterns.length > 0">
|
||||||
|
<div v-for="(pattern, index) in patterns" class="ui label basic small" :class="{blue: index == editingIndex, disabled: isAdding && index != editingIndex}" style="margin-bottom: 0.8em">
|
||||||
|
<span class="grey" style="font-weight: normal">[{{patternTypeName(pattern.type)}}]</span> <span >{{pattern.pattern}}</span>
|
||||||
|
<a href="" title="修改" @click.prevent="edit(index)"><i class="icon pencil tiny"></i></a>
|
||||||
|
<a href="" title="删除" @click.prevent="remove(index)"><i class="icon remove small"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-show="isAdding" style="margin-top: 0.5em">
|
||||||
|
<div class="ui fields inline">
|
||||||
|
<div class="ui field">
|
||||||
|
<select class="ui dropdown auto-width" v-model="addingPattern.type">
|
||||||
|
<option value="wildcard">通配符</option>
|
||||||
|
<option value="regexp">正则表达式</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="ui field">
|
||||||
|
<input type="text" :placeholder="(addingPattern.type == 'wildcard') ? '可以使用星号(*)通配符,不区分大小写' : '可以使用正则表达式,不区分大小写'" v-model="addingPattern.pattern" size="36" ref="patternInput" @keyup.enter="confirm()" @keypress.enter.prevent="1" spellcheck="false"/>
|
||||||
|
</div>
|
||||||
|
<div class="ui field" style="padding-left: 0">
|
||||||
|
<tip-icon content="通配符示例:<br/>单个路径开头:/hello/world/*<br/>单个路径结尾:*/hello/world<br/>包含某个路径:*/article/*<br/>某个域名下的所有URL:*example.com/*" v-if="addingPattern.type == 'wildcard'"></tip-icon>
|
||||||
|
<tip-icon content="正则表达式示例:<br/>单个路径开头:^/hello/world<br/>单个路径结尾:/hello/world$<br/>包含某个路径:/article/<br/>匹配某个数字路径:/article/(\\d+)<br/>某个域名下的所有URL:^(http|https)://example.com/" v-if="addingPattern.type == 'regexp'"></tip-icon>
|
||||||
|
</div>
|
||||||
|
<div class="ui field">
|
||||||
|
<button class="ui button tiny" type="button" @click.prevent="confirm">确定</button><a href="" title="取消" @click.prevent="cancel"><i class="icon remove small"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if=!isAdding style="margin-top: 0.5em">
|
||||||
|
<button class="ui button tiny basic" type="button" @click.prevent="add">+</button>
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
})
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
Vue.component("values-box", {
|
Vue.component("values-box", {
|
||||||
props: ["values", "v-values", "size", "maxlength", "name", "placeholder", "v-allow-empty"],
|
props: ["values", "v-values", "size", "maxlength", "name", "placeholder", "v-allow-empty", "validator"],
|
||||||
data: function () {
|
data: function () {
|
||||||
let values = this.values;
|
let values = this.values;
|
||||||
if (values == null) {
|
if (values == null) {
|
||||||
@@ -44,6 +44,22 @@ Vue.component("values-box", {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validate
|
||||||
|
if (typeof(this.validator) == "function") {
|
||||||
|
let resp = this.validator.call(this, this.value)
|
||||||
|
if (typeof resp == "object") {
|
||||||
|
if (typeof resp.isOk == "boolean" && !resp.isOk) {
|
||||||
|
if (typeof resp.message == "string") {
|
||||||
|
let that = this
|
||||||
|
teaweb.warn(resp.message, function () {
|
||||||
|
that.$refs.value.focus();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isUpdating) {
|
if (this.isUpdating) {
|
||||||
Vue.set(this.realValues, this.index, this.value);
|
Vue.set(this.realValues, this.index, this.value);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
Vue.component("node-ip-address-clusters-selector", {
|
||||||
|
props: ["vClusters"],
|
||||||
|
mounted: function () {
|
||||||
|
this.checkClusters()
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
let clusters = this.vClusters
|
||||||
|
if (clusters == null) {
|
||||||
|
clusters = []
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
clusters: clusters,
|
||||||
|
hasCheckedCluster: false,
|
||||||
|
clustersVisible: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
checkClusters: function () {
|
||||||
|
let that = this
|
||||||
|
|
||||||
|
let b = false
|
||||||
|
this.clusters.forEach(function (cluster) {
|
||||||
|
if (cluster.isChecked) {
|
||||||
|
b = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.hasCheckedCluster = b
|
||||||
|
|
||||||
|
return b
|
||||||
|
},
|
||||||
|
changeCluster: function (cluster) {
|
||||||
|
cluster.isChecked = !cluster.isChecked
|
||||||
|
this.checkClusters()
|
||||||
|
},
|
||||||
|
showClusters: function () {
|
||||||
|
this.clustersVisible = !this.clustersVisible
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `<div>
|
||||||
|
<span v-if="!hasCheckedCluster">默认用于所有集群 <a href="" @click.prevent="showClusters">修改 <i class="icon angle" :class="{down: !clustersVisible, up:clustersVisible}"></i></a></span>
|
||||||
|
<div v-if="hasCheckedCluster">
|
||||||
|
<span v-for="cluster in clusters" class="ui label basic small" v-if="cluster.isChecked">{{cluster.name}}</span> <a href="" @click.prevent="showClusters">修改 <i class="icon angle" :class="{down: !clustersVisible, up:clustersVisible}"></i></a>
|
||||||
|
<p class="comment">当前IP仅在所选集群中有效。</p>
|
||||||
|
</div>
|
||||||
|
<div v-show="clustersVisible">
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<checkbox v-for="cluster in clusters" :v-value="cluster.id" :value="cluster.isChecked ? cluster.id : 0" style="margin-right: 1em" @input="changeCluster(cluster)" name="clusterIds">
|
||||||
|
{{cluster.name}}
|
||||||
|
</checkbox>
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
})
|
||||||
@@ -1,10 +1,16 @@
|
|||||||
// 节点IP地址管理(标签形式)
|
// 节点IP地址管理(标签形式)
|
||||||
Vue.component("node-ip-addresses-box", {
|
Vue.component("node-ip-addresses-box", {
|
||||||
props: ["v-ip-addresses", "role"],
|
props: ["v-ip-addresses", "role", "v-node-id"],
|
||||||
data: function () {
|
data: function () {
|
||||||
|
let nodeId = this.vNodeId
|
||||||
|
if (nodeId == null) {
|
||||||
|
nodeId = 0
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ipAddresses: (this.vIpAddresses == null) ? [] : this.vIpAddresses,
|
ipAddresses: (this.vIpAddresses == null) ? [] : this.vIpAddresses,
|
||||||
supportThresholds: this.role != "ns"
|
supportThresholds: this.role != "ns",
|
||||||
|
nodeId: nodeId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -13,7 +19,7 @@ Vue.component("node-ip-addresses-box", {
|
|||||||
window.UPDATING_NODE_IP_ADDRESS = null
|
window.UPDATING_NODE_IP_ADDRESS = null
|
||||||
|
|
||||||
let that = this;
|
let that = this;
|
||||||
teaweb.popup("/nodes/ipAddresses/createPopup?supportThresholds=" + (this.supportThresholds ? 1 : 0), {
|
teaweb.popup("/nodes/ipAddresses/createPopup?nodeId=" + this.nodeId + "&supportThresholds=" + (this.supportThresholds ? 1 : 0), {
|
||||||
callback: function (resp) {
|
callback: function (resp) {
|
||||||
that.ipAddresses.push(resp.data.ipAddress);
|
that.ipAddresses.push(resp.data.ipAddress);
|
||||||
},
|
},
|
||||||
@@ -24,10 +30,10 @@ Vue.component("node-ip-addresses-box", {
|
|||||||
|
|
||||||
// 修改地址
|
// 修改地址
|
||||||
updateIPAddress: function (index, address) {
|
updateIPAddress: function (index, address) {
|
||||||
window.UPDATING_NODE_IP_ADDRESS = address
|
window.UPDATING_NODE_IP_ADDRESS = teaweb.clone(address)
|
||||||
|
|
||||||
let that = this;
|
let that = this;
|
||||||
teaweb.popup("/nodes/ipAddresses/updatePopup?supportThresholds=" + (this.supportThresholds ? 1 : 0), {
|
teaweb.popup("/nodes/ipAddresses/updatePopup?nodeId=" + this.nodeId + "&supportThresholds=" + (this.supportThresholds ? 1 : 0), {
|
||||||
callback: function (resp) {
|
callback: function (resp) {
|
||||||
Vue.set(that.ipAddresses, index, resp.data.ipAddress);
|
Vue.set(that.ipAddresses, index, resp.data.ipAddress);
|
||||||
},
|
},
|
||||||
@@ -58,6 +64,11 @@ Vue.component("node-ip-addresses-box", {
|
|||||||
<span class="small red" v-if="!address.isUp" title="已下线">[down]</span>
|
<span class="small red" v-if="!address.isUp" title="已下线">[down]</span>
|
||||||
<span class="small" v-if="address.thresholds != null && address.thresholds.length > 0">[{{address.thresholds.length}}个阈值]</span>
|
<span class="small" v-if="address.thresholds != null && address.thresholds.length > 0">[{{address.thresholds.length}}个阈值]</span>
|
||||||
|
|
||||||
|
<span v-if="address.clusters != null && address.clusters.length > 0">
|
||||||
|
<span class="small grey">专属集群:[</span><span v-for="(cluster, index) in address.clusters" class="small grey">{{cluster.name}}<span v-if="index < address.clusters.length - 1">,</span></span><span class="small grey">]</span>
|
||||||
|
|
||||||
|
</span>
|
||||||
|
|
||||||
<a href="" title="修改" @click.prevent="updateIPAddress(index, address)"><i class="icon pencil small"></i></a>
|
<a href="" title="修改" @click.prevent="updateIPAddress(index, address)"><i class="icon pencil small"></i></a>
|
||||||
<a href="" title="删除" @click.prevent="removeIPAddress(index)"><i class="icon remove"></i></a>
|
<a href="" title="删除" @click.prevent="removeIPAddress(index)"><i class="icon remove"></i></a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ Vue.component("firewall-syn-flood-config-box", {
|
|||||||
<input type="hidden" name="synFloodJSON" :value="JSON.stringify(config)"/>
|
<input type="hidden" name="synFloodJSON" :value="JSON.stringify(config)"/>
|
||||||
<a href="" @click.prevent="edit">
|
<a href="" @click.prevent="edit">
|
||||||
<span v-if="config.isOn">
|
<span v-if="config.isOn">
|
||||||
已启用 / <span>空连接次数:{{config.minAttempts}}次/分钟</span> / 封禁时间:{{config.timeoutSeconds}}秒 <span v-if="config.ignoreLocal">/ 忽略局域网访问</span>
|
已启用 / <span>空连接次数:{{config.minAttempts}}次/分钟</span> / 封禁时长:{{config.timeoutSeconds}}秒 <span v-if="config.ignoreLocal">/ 忽略局域网访问</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-else>未启用</span>
|
<span v-else>未启用</span>
|
||||||
<i class="icon angle" :class="{up: isEditing, down: !isEditing}"></i>
|
<i class="icon angle" :class="{up: isEditing, down: !isEditing}"></i>
|
||||||
@@ -73,7 +73,7 @@ Vue.component("firewall-syn-flood-config-box", {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>封禁时间</td>
|
<td>封禁时长</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="ui input right labeled">
|
<div class="ui input right labeled">
|
||||||
<input type="text" v-model="timeoutSeconds" style="width: 5em" maxlength="8"/>
|
<input type="text" v-model="timeoutSeconds" style="width: 5em" maxlength="8"/>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ Vue.component("firewall-syn-flood-config-viewer", {
|
|||||||
},
|
},
|
||||||
template: `<div>
|
template: `<div>
|
||||||
<span v-if="config.isOn">
|
<span v-if="config.isOn">
|
||||||
已启用 / <span>空连接次数:{{config.minAttempts}}次/分钟</span> / 封禁时间:{{config.timeoutSeconds}}秒 <span v-if="config.ignoreLocal">/ 忽略局域网访问</span>
|
已启用 / <span>空连接次数:{{config.minAttempts}}次/分钟</span> / 封禁时长:{{config.timeoutSeconds}}秒 <span v-if="config.ignoreLocal">/ 忽略局域网访问</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-else>未启用</span>
|
<span v-else>未启用</span>
|
||||||
</div>`
|
</div>`
|
||||||
|
|||||||
@@ -56,18 +56,22 @@ Vue.component("http-cache-config-box", {
|
|||||||
},
|
},
|
||||||
template: `<div>
|
template: `<div>
|
||||||
<input type="hidden" name="cacheJSON" :value="JSON.stringify(cacheConfig)"/>
|
<input type="hidden" name="cacheJSON" :value="JSON.stringify(cacheConfig)"/>
|
||||||
|
|
||||||
|
<table class="ui table definition selectable" v-show="!vIsGroup">
|
||||||
|
<tr>
|
||||||
|
<td class="title">全局缓存策略</td>
|
||||||
|
<td>
|
||||||
|
<div v-if="vCachePolicy != null">{{vCachePolicy.name}} <link-icon :href="'/servers/components/cache/policy?cachePolicyId=' + vCachePolicy.id"></link-icon>
|
||||||
|
<p class="comment">使用当前服务所在集群的设置。</p>
|
||||||
|
</div>
|
||||||
|
<span v-else class="red">当前集群没有设置缓存策略,当前配置无法生效。</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
<table class="ui table definition selectable">
|
<table class="ui table definition selectable">
|
||||||
<prior-checkbox :v-config="cacheConfig" v-if="vIsLocation || vIsGroup"></prior-checkbox>
|
<prior-checkbox :v-config="cacheConfig" v-if="vIsLocation || vIsGroup"></prior-checkbox>
|
||||||
<tbody v-show="(!vIsLocation && !vIsGroup) || cacheConfig.isPrior">
|
<tbody v-show="(!vIsLocation && !vIsGroup) || cacheConfig.isPrior">
|
||||||
<tr v-show="!vIsGroup">
|
|
||||||
<td>缓存策略</td>
|
|
||||||
<td>
|
|
||||||
<div v-if="vCachePolicy != null">{{vCachePolicy.name}} <link-icon :href="'/servers/components/cache/policy?cachePolicyId=' + vCachePolicy.id"></link-icon>
|
|
||||||
<p class="comment">使用当前服务所在集群的设置。</p>
|
|
||||||
</div>
|
|
||||||
<span v-else class="red">当前集群没有设置缓存策略,当前配置无法生效。</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="title">启用缓存</td>
|
<td class="title">启用缓存</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -90,7 +94,7 @@ Vue.component("http-cache-config-box", {
|
|||||||
<td>使用默认缓存条件</td>
|
<td>使用默认缓存条件</td>
|
||||||
<td>
|
<td>
|
||||||
<checkbox v-model="enablePolicyRefs"></checkbox>
|
<checkbox v-model="enablePolicyRefs"></checkbox>
|
||||||
<p class="comment">选中后使用系统中已经定义的默认缓存条件。</p>
|
<p class="comment">选中后使用系统全局缓存策略中已经定义的默认缓存条件。</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -190,7 +190,10 @@ Vue.component("http-cache-ref-box", {
|
|||||||
<tr v-if="condCategory == 'simple'">
|
<tr v-if="condCategory == 'simple'">
|
||||||
<td class="color-border">{{condComponent.paramsTitle}} *</td>
|
<td class="color-border">{{condComponent.paramsTitle}} *</td>
|
||||||
<td>
|
<td>
|
||||||
<component :is="condComponent.component" :v-cond="ref.simpleCond"></component>
|
<component :is="condComponent.component" :v-cond="ref.simpleCond" v-if="condComponent.type != 'params'"></component>
|
||||||
|
<table class="ui table" v-if="condComponent.type == 'params'">
|
||||||
|
<component :is="condComponent.component" :v-cond="ref.simpleCond"></component>
|
||||||
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="condCategory == 'simple' && condComponent.caseInsensitive">
|
<tr v-if="condCategory == 'simple' && condComponent.caseInsensitive">
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ Vue.component("http-cache-refs-config-box", {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
updateRef: function (index, cacheRef) {
|
updateRef: function (index, cacheRef) {
|
||||||
window.UPDATING_CACHE_REF = cacheRef
|
window.UPDATING_CACHE_REF = teaweb.clone(cacheRef)
|
||||||
|
|
||||||
let height = window.innerHeight
|
let height = window.innerHeight
|
||||||
if (height > 500) {
|
if (height > 500) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Vue.component("http-cond-url-extension", {
|
|||||||
data: function () {
|
data: function () {
|
||||||
let cond = {
|
let cond = {
|
||||||
isRequest: true,
|
isRequest: true,
|
||||||
param: "${requestPathExtension}",
|
param: "${requestPathLowerExtension}",
|
||||||
operator: "in",
|
operator: "in",
|
||||||
value: "[]"
|
value: "[]"
|
||||||
}
|
}
|
||||||
@@ -95,7 +95,7 @@ Vue.component("http-cond-url-not-extension", {
|
|||||||
data: function () {
|
data: function () {
|
||||||
let cond = {
|
let cond = {
|
||||||
isRequest: true,
|
isRequest: true,
|
||||||
param: "${requestPathExtension}",
|
param: "${requestPathLowerExtension}",
|
||||||
operator: "not in",
|
operator: "not in",
|
||||||
value: "[]"
|
value: "[]"
|
||||||
}
|
}
|
||||||
@@ -718,21 +718,19 @@ Vue.component("http-cond-params", {
|
|||||||
},
|
},
|
||||||
template: `<tbody>
|
template: `<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>参数值</td>
|
<td style="width: 8em">参数值</td>
|
||||||
<td>
|
<td>
|
||||||
<input type="hidden" name="condJSON" :value="JSON.stringify(cond)"/>
|
<input type="hidden" name="condJSON" :value="JSON.stringify(cond)"/>
|
||||||
<div>
|
<div>
|
||||||
<div class="ui fields inline">
|
<div class="ui field">
|
||||||
<div class="ui field">
|
<input type="text" placeholder="\${xxx}" v-model="cond.param"/>
|
||||||
<input type="text" placeholder="\${xxx}" v-model="cond.param"/>
|
</div>
|
||||||
</div>
|
<div class="ui field">
|
||||||
<div class="ui field">
|
<select class="ui dropdown" style="width: 16em; color: grey" v-model="variable" @change="changeVariable">
|
||||||
<select class="ui dropdown" style="width: 7em; color: grey" v-model="variable" @change="changeVariable">
|
<option value="">[常用参数]</option>
|
||||||
<option value="">[常用参数]</option>
|
<option v-for="v in variables" :value="v.code">{{v.code}} - {{v.name}}</option>
|
||||||
<option v-for="v in variables" :value="v.code">{{v.code}} - {{v.name}}</option>
|
</select>
|
||||||
</select>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="comment">其中可以使用变量,类似于<code-label>\${requestPath}</code-label>,也可以是多个变量的组合。</p>
|
<p class="comment">其中可以使用变量,类似于<code-label>\${requestPath}</code-label>,也可以是多个变量的组合。</p>
|
||||||
</td>
|
</td>
|
||||||
@@ -835,5 +833,6 @@ Vue.component("http-cond-params", {
|
|||||||
<p class="comment">选中后表示对比时忽略参数值的大小写。</p>
|
<p class="comment">选中后表示对比时忽略参数值的大小写。</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>`
|
</tbody>
|
||||||
|
`
|
||||||
})
|
})
|
||||||
@@ -73,6 +73,7 @@ Vue.component("http-firewall-actions-box", {
|
|||||||
|
|
||||||
// 动作参数
|
// 动作参数
|
||||||
blockTimeout: "",
|
blockTimeout: "",
|
||||||
|
blockTimeoutMax: "",
|
||||||
blockScope: "global",
|
blockScope: "global",
|
||||||
|
|
||||||
captchaLife: "",
|
captchaLife: "",
|
||||||
@@ -122,6 +123,14 @@ Vue.component("http-firewall-actions-box", {
|
|||||||
this.actionOptions["timeout"] = v
|
this.actionOptions["timeout"] = v
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
blockTimeoutMax: function (v) {
|
||||||
|
v = parseInt(v)
|
||||||
|
if (isNaN(v)) {
|
||||||
|
this.actionOptions["timeoutMax"] = 0
|
||||||
|
} else {
|
||||||
|
this.actionOptions["timeoutMax"] = v
|
||||||
|
}
|
||||||
|
},
|
||||||
blockScope: function (v) {
|
blockScope: function (v) {
|
||||||
this.actionOptions["scope"] = v
|
this.actionOptions["scope"] = v
|
||||||
},
|
},
|
||||||
@@ -237,6 +246,7 @@ Vue.component("http-firewall-actions-box", {
|
|||||||
|
|
||||||
// 动作参数
|
// 动作参数
|
||||||
this.blockTimeout = ""
|
this.blockTimeout = ""
|
||||||
|
this.blockTimeoutMax = ""
|
||||||
this.blockScope = "global"
|
this.blockScope = "global"
|
||||||
|
|
||||||
this.captchaLife = ""
|
this.captchaLife = ""
|
||||||
@@ -298,9 +308,13 @@ Vue.component("http-firewall-actions-box", {
|
|||||||
switch (config.code) {
|
switch (config.code) {
|
||||||
case "block":
|
case "block":
|
||||||
this.blockTimeout = ""
|
this.blockTimeout = ""
|
||||||
|
this.blockTimeoutMax = ""
|
||||||
if (config.options.timeout != null || config.options.timeout > 0) {
|
if (config.options.timeout != null || config.options.timeout > 0) {
|
||||||
this.blockTimeout = config.options.timeout.toString()
|
this.blockTimeout = config.options.timeout.toString()
|
||||||
}
|
}
|
||||||
|
if (config.options.timeoutMax != null || config.options.timeoutMax > 0) {
|
||||||
|
this.blockTimeoutMax = config.options.timeoutMax.toString()
|
||||||
|
}
|
||||||
if (config.options.scope != null && config.options.scope.length > 0) {
|
if (config.options.scope != null && config.options.scope.length > 0) {
|
||||||
this.blockScope = config.options.scope
|
this.blockScope = config.options.scope
|
||||||
} else {
|
} else {
|
||||||
@@ -584,7 +598,7 @@ Vue.component("http-firewall-actions-box", {
|
|||||||
{{config.name}} <span class="small">({{config.code.toUpperCase()}})</span>
|
{{config.name}} <span class="small">({{config.code.toUpperCase()}})</span>
|
||||||
|
|
||||||
<!-- block -->
|
<!-- block -->
|
||||||
<span v-if="config.code == 'block' && config.options.timeout > 0">:有效期{{config.options.timeout}}秒</span>
|
<span v-if="config.code == 'block' && config.options.timeout > 0">:封禁时长{{config.options.timeout}}<span v-if="config.options.timeoutMax > config.options.timeout">-{{config.options.timeoutMax}}</span>秒</span>
|
||||||
|
|
||||||
<!-- captcha -->
|
<!-- captcha -->
|
||||||
<span v-if="config.code == 'captcha' && config.options.life > 0">:有效期{{config.options.life}}秒
|
<span v-if="config.code == 'captcha' && config.options.life > 0">:有效期{{config.options.life}}秒
|
||||||
@@ -643,7 +657,18 @@ Vue.component("http-firewall-actions-box", {
|
|||||||
|
|
||||||
<!-- block -->
|
<!-- block -->
|
||||||
<tr v-if="actionCode == 'block'">
|
<tr v-if="actionCode == 'block'">
|
||||||
<td>封锁时间</td>
|
<td>封禁范围</td>
|
||||||
|
<td>
|
||||||
|
<select class="ui dropdown auto-width" v-model="blockScope">
|
||||||
|
<option value="service">当前服务</option>
|
||||||
|
<option value="global">所有服务</option>
|
||||||
|
</select>
|
||||||
|
<p class="comment" v-if="blockScope == 'service'">只封禁用户对当前网站服务的访问,其他服务不受影响。</p>
|
||||||
|
<p class="comment" v-if="blockScope =='global'">封禁用户对所有网站服务的访问。</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="actionCode == 'block'">
|
||||||
|
<td>封禁时长</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="ui input right labeled">
|
<div class="ui input right labeled">
|
||||||
<input type="text" style="width: 5em" maxlength="9" v-model="blockTimeout" @keyup.enter="confirm()" @keypress.enter.prevent="1"/>
|
<input type="text" style="width: 5em" maxlength="9" v-model="blockTimeout" @keyup.enter="confirm()" @keypress.enter.prevent="1"/>
|
||||||
@@ -652,14 +677,13 @@ Vue.component("http-firewall-actions-box", {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="actionCode == 'block'">
|
<tr v-if="actionCode == 'block'">
|
||||||
<td>封锁范围</td>
|
<td>最大封禁时长</td>
|
||||||
<td>
|
<td>
|
||||||
<select class="ui dropdown auto-width" v-model="blockScope">
|
<div class="ui input right labeled">
|
||||||
<option value="service">当前服务</option>
|
<input type="text" style="width: 5em" maxlength="9" v-model="blockTimeoutMax" @keyup.enter="confirm()" @keypress.enter.prevent="1"/>
|
||||||
<option value="global">所有服务</option>
|
<span class="ui label">秒</span>
|
||||||
</select>
|
</div>
|
||||||
<p class="comment" v-if="blockScope == 'service'">只封锁用户对当前网站服务的访问,其他服务不受影响。</p>
|
<p class="comment">选填项。如果同时填写了封禁时长和最大封禁时长,则会在两者之间随机选择一个数字作为最终的封禁时长。</p>
|
||||||
<p class="comment" v-if="blockScope =='global'">封锁用户对所有网站服务的访问。</p>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Vue.component("http-firewall-block-options-viewer", {
|
|||||||
template: `<div>
|
template: `<div>
|
||||||
<span v-if="options == null">默认设置</span>
|
<span v-if="options == null">默认设置</span>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
状态码:{{options.statusCode}} / 提示内容:<span v-if="options.body != null && options.body.length > 0">[{{options.body.length}}字符]</span><span v-else class="disabled">[无]</span> / 超时时间:{{options.timeout}}秒
|
状态码:{{options.statusCode}} / 提示内容:<span v-if="options.body != null && options.body.length > 0">[{{options.body.length}}字符]</span><span v-else class="disabled">[无]</span> / 超时时间:{{options.timeout}}秒 <span v-if="options.timeoutMax > options.timeout">/ 最大封禁时长:{{options.timeoutMax}}秒</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ Vue.component("http-firewall-block-options", {
|
|||||||
blockOptions: this.vBlockOptions,
|
blockOptions: this.vBlockOptions,
|
||||||
statusCode: this.vBlockOptions.statusCode,
|
statusCode: this.vBlockOptions.statusCode,
|
||||||
timeout: this.vBlockOptions.timeout,
|
timeout: this.vBlockOptions.timeout,
|
||||||
|
timeoutMax: this.vBlockOptions.timeoutMax,
|
||||||
isEditing: false
|
isEditing: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -24,6 +25,14 @@ Vue.component("http-firewall-block-options", {
|
|||||||
} else {
|
} else {
|
||||||
this.blockOptions.timeout = timeout
|
this.blockOptions.timeout = timeout
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
timeoutMax: function (v) {
|
||||||
|
let timeoutMax = parseInt(v)
|
||||||
|
if (isNaN(timeoutMax)) {
|
||||||
|
this.blockOptions.timeoutMax = 0
|
||||||
|
} else {
|
||||||
|
this.blockOptions.timeoutMax = timeoutMax
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -33,7 +42,9 @@ Vue.component("http-firewall-block-options", {
|
|||||||
},
|
},
|
||||||
template: `<div>
|
template: `<div>
|
||||||
<input type="hidden" name="blockOptionsJSON" :value="JSON.stringify(blockOptions)"/>
|
<input type="hidden" name="blockOptionsJSON" :value="JSON.stringify(blockOptions)"/>
|
||||||
<a href="" @click.prevent="edit">状态码:{{statusCode}} / 提示内容:<span v-if="blockOptions.body != null && blockOptions.body.length > 0">[{{blockOptions.body.length}}字符]</span><span v-else class="disabled">[无]</span> / 超时时间:{{timeout}}秒 <i class="icon angle" :class="{up: isEditing, down: !isEditing}"></i></a>
|
<a href="" @click.prevent="edit">状态码:{{statusCode}} / 提示内容:<span v-if="blockOptions.body != null && blockOptions.body.length > 0">[{{blockOptions.body.length}}字符]</span><span v-else class="disabled">[无]</span> <span v-if="timeout > 0"> / 封禁时长:{{timeout}}秒</span>
|
||||||
|
<span v-if="timeoutMax > timeout"> / 最大封禁时长:{{timeoutMax}}秒</span>
|
||||||
|
<i class="icon angle" :class="{up: isEditing, down: !isEditing}"></i></a>
|
||||||
<table class="ui table" v-show="isEditing">
|
<table class="ui table" v-show="isEditing">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="title">状态码</td>
|
<td class="title">状态码</td>
|
||||||
@@ -48,13 +59,23 @@ Vue.component("http-firewall-block-options", {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>超时时间</td>
|
<td>封禁时长</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="ui input right labeled">
|
<div class="ui input right labeled">
|
||||||
<input type="text" v-model="timeout" style="width: 5em" maxlength="6"/>
|
<input type="text" v-model="timeout" style="width: 5em" maxlength="6"/>
|
||||||
<span class="ui label">秒</span>
|
<span class="ui label">秒</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="comment">触发阻止动作时,封锁客户端IP的时间。</p>
|
<p class="comment">触发阻止动作时,封禁客户端IP的时间。</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>最大封禁时长</td>
|
||||||
|
<td>
|
||||||
|
<div class="ui input right labeled">
|
||||||
|
<input type="text" v-model="timeoutMax" style="width: 5em" maxlength="6"/>
|
||||||
|
<span class="ui label">秒</span>
|
||||||
|
</div>
|
||||||
|
<p class="comment">如果最大封禁时长大于封禁时长({{timeout}}秒),那么表示每次封禁的时候,将会在这两个时长数字之间随机选取一个数字作为最终的封禁时长。</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -6,36 +6,60 @@ Vue.component("http-firewall-config-box", {
|
|||||||
firewall = {
|
firewall = {
|
||||||
isPrior: false,
|
isPrior: false,
|
||||||
isOn: false,
|
isOn: false,
|
||||||
firewallPolicyId: 0
|
firewallPolicyId: 0,
|
||||||
|
ignoreGlobalRules: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
firewall: firewall
|
firewall: firewall,
|
||||||
|
moreOptionsVisible: false,
|
||||||
|
execGlobalRules: !firewall.ignoreGlobalRules
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
execGlobalRules: function (v) {
|
||||||
|
this.firewall.ignoreGlobalRules = !v
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
changeOptionsVisible: function (v) {
|
||||||
|
this.moreOptionsVisible = v
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
template: `<div>
|
template: `<div>
|
||||||
<input type="hidden" name="firewallJSON" :value="JSON.stringify(firewall)"/>
|
<input type="hidden" name="firewallJSON" :value="JSON.stringify(firewall)"/>
|
||||||
|
|
||||||
|
<table class="ui table selectable definition" v-show="!vIsGroup">
|
||||||
|
<tr>
|
||||||
|
<td class="title">全局WAF策略</td>
|
||||||
|
<td>
|
||||||
|
<div v-if="vFirewallPolicy != null">{{vFirewallPolicy.name}} <span v-if="vFirewallPolicy.modeInfo != null"> <span :class="{green: vFirewallPolicy.modeInfo.code == 'defend', blue: vFirewallPolicy.modeInfo.code == 'observe', grey: vFirewallPolicy.modeInfo.code == 'bypass'}">[{{vFirewallPolicy.modeInfo.name}}]</span> </span> <link-icon :href="'/servers/components/waf/policy?firewallPolicyId=' + vFirewallPolicy.id"></link-icon>
|
||||||
|
<p class="comment">当前服务所在集群的设置。</p>
|
||||||
|
</div>
|
||||||
|
<span v-else class="red">当前集群没有设置WAF策略,当前配置无法生效。</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
<table class="ui table selectable definition">
|
<table class="ui table selectable definition">
|
||||||
<prior-checkbox :v-config="firewall" v-if="vIsLocation || vIsGroup"></prior-checkbox>
|
<prior-checkbox :v-config="firewall" v-if="vIsLocation || vIsGroup"></prior-checkbox>
|
||||||
<tbody v-show="(!vIsLocation && !vIsGroup) || firewall.isPrior">
|
<tbody v-show="(!vIsLocation && !vIsGroup) || firewall.isPrior">
|
||||||
<tr v-show="!vIsGroup">
|
|
||||||
<td>WAF策略</td>
|
|
||||||
<td>
|
|
||||||
<div v-if="vFirewallPolicy != null">{{vFirewallPolicy.name}} <span v-if="vFirewallPolicy.modeInfo != null"> <span :class="{green: vFirewallPolicy.modeInfo.code == 'defend', blue: vFirewallPolicy.modeInfo.code == 'observe', grey: vFirewallPolicy.modeInfo.code == 'bypass'}">[{{vFirewallPolicy.modeInfo.name}}]</span> </span> <link-icon :href="'/servers/components/waf/policy?firewallPolicyId=' + vFirewallPolicy.id"></link-icon>
|
|
||||||
<p class="comment">使用当前服务所在集群的设置。</p>
|
|
||||||
</div>
|
|
||||||
<span v-else class="red">当前集群没有设置WAF策略,当前配置无法生效。</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="title">启用WAF</td>
|
<td class="title">启用WAF</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="ui checkbox">
|
<checkbox v-model="firewall.isOn"></checkbox>
|
||||||
<input type="checkbox" v-model="firewall.isOn"/>
|
<p class="comment">选中后,表示启用当前网站服务的WAF功能。</p>
|
||||||
<label></label>
|
</td>
|
||||||
</div>
|
</tr>
|
||||||
<p class="comment">启用WAF之后,各项WAF设置才会生效。</p>
|
</tbody>
|
||||||
|
<more-options-tbody @change="changeOptionsVisible"></more-options-tbody>
|
||||||
|
<tbody v-show="moreOptionsVisible">
|
||||||
|
<tr>
|
||||||
|
<td>启用系统全局规则</td>
|
||||||
|
<td>
|
||||||
|
<checkbox v-model="execGlobalRules"></checkbox>
|
||||||
|
<p class="comment">选中后,表示使用系统全局WAF策略中定义的规则。</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ Vue.component("http-firewall-rule-label", {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return operatorName
|
return operatorName
|
||||||
|
},
|
||||||
|
isEmptyString: function (v) {
|
||||||
|
return typeof v == "string" && v.length == 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
template: `<div>
|
template: `<div>
|
||||||
@@ -40,8 +43,9 @@ Vue.component("http-firewall-rule-label", {
|
|||||||
|
|
||||||
<span v-else>
|
<span v-else>
|
||||||
<span v-if="rule.paramFilters != null && rule.paramFilters.length > 0" v-for="paramFilter in rule.paramFilters"> | {{paramFilter.code}}</span>
|
<span v-if="rule.paramFilters != null && rule.paramFilters.length > 0" v-for="paramFilter in rule.paramFilters"> | {{paramFilter.code}}</span>
|
||||||
<span :class="{dash:rule.isCaseInsensitive}" :title="rule.isCaseInsensitive ? '大小写不敏感':''" v-if="!rule.isComposed">{{operatorName(rule.operator)}}</span>
|
<span :class="{dash:!rule.isComposed && rule.isCaseInsensitive}" :title="(!rule.isComposed && rule.isCaseInsensitive) ? '大小写不敏感':''">{{operatorName(rule.operator)}}</span>
|
||||||
{{rule.value}}
|
<span v-if="!isEmptyString(rule.value)">{{rule.value}}</span>
|
||||||
|
<span v-else class="disabled" style="font-weight: normal" title="空字符串">[空]</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- description -->
|
<!-- description -->
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ Vue.component("http-firewall-rules-box", {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return operatorName
|
return operatorName
|
||||||
|
},
|
||||||
|
isEmptyString: function (v) {
|
||||||
|
return typeof v == "string" && v.length == 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
template: `<div>
|
template: `<div>
|
||||||
@@ -67,7 +70,9 @@ Vue.component("http-firewall-rules-box", {
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span v-else>
|
<span v-else>
|
||||||
<span v-if="rule.paramFilters != null && rule.paramFilters.length > 0" v-for="paramFilter in rule.paramFilters"> | {{paramFilter.code}}</span> <span :class="{dash:rule.isCaseInsensitive}" :title="rule.isCaseInsensitive ? '大小写不敏感':''">{{operatorName(rule.operator)}}</span> {{rule.value}}
|
<span v-if="rule.paramFilters != null && rule.paramFilters.length > 0" v-for="paramFilter in rule.paramFilters"> | {{paramFilter.code}}</span> <span :class="{dash:(!rule.isComposed && rule.isCaseInsensitive)}" :title="(!rule.isComposed && rule.isCaseInsensitive) ? '大小写不敏感':''">{{operatorName(rule.operator)}}</span>
|
||||||
|
<span v-if="!isEmptyString(rule.value)">{{rule.value}}</span>
|
||||||
|
<span v-else class="disabled" style="font-weight: normal" title="空字符串">[空]</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- description -->
|
<!-- description -->
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ Vue.component("http-firewall-checkpoint-cc", {
|
|||||||
let period = 60
|
let period = 60
|
||||||
let threshold = 1000
|
let threshold = 1000
|
||||||
let ignoreCommonFiles = false
|
let ignoreCommonFiles = false
|
||||||
|
let enableFingerprint = true
|
||||||
|
|
||||||
let options = {}
|
let options = {}
|
||||||
if (window.parent.UPDATING_RULE != null) {
|
if (window.parent.UPDATING_RULE != null) {
|
||||||
@@ -117,6 +118,9 @@ Vue.component("http-firewall-checkpoint-cc", {
|
|||||||
if (options.ignoreCommonFiles != null && typeof (options.ignoreCommonFiles) == "boolean") {
|
if (options.ignoreCommonFiles != null && typeof (options.ignoreCommonFiles) == "boolean") {
|
||||||
ignoreCommonFiles = options.ignoreCommonFiles
|
ignoreCommonFiles = options.ignoreCommonFiles
|
||||||
}
|
}
|
||||||
|
if (options.enableFingerprint != null && typeof (options.enableFingerprint) == "boolean") {
|
||||||
|
enableFingerprint = options.enableFingerprint
|
||||||
|
}
|
||||||
|
|
||||||
let that = this
|
let that = this
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
@@ -128,6 +132,7 @@ Vue.component("http-firewall-checkpoint-cc", {
|
|||||||
period: period,
|
period: period,
|
||||||
threshold: threshold,
|
threshold: threshold,
|
||||||
ignoreCommonFiles: ignoreCommonFiles,
|
ignoreCommonFiles: ignoreCommonFiles,
|
||||||
|
enableFingerprint: enableFingerprint,
|
||||||
options: {},
|
options: {},
|
||||||
value: threshold
|
value: threshold
|
||||||
}
|
}
|
||||||
@@ -141,6 +146,9 @@ Vue.component("http-firewall-checkpoint-cc", {
|
|||||||
},
|
},
|
||||||
ignoreCommonFiles: function () {
|
ignoreCommonFiles: function () {
|
||||||
this.change()
|
this.change()
|
||||||
|
},
|
||||||
|
enableFingerprint: function () {
|
||||||
|
this.change()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -165,6 +173,11 @@ Vue.component("http-firewall-checkpoint-cc", {
|
|||||||
ignoreCommonFiles = false
|
ignoreCommonFiles = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let enableFingerprint = this.enableFingerprint
|
||||||
|
if (typeof enableFingerprint != "boolean") {
|
||||||
|
enableFingerprint = true
|
||||||
|
}
|
||||||
|
|
||||||
this.vCheckpoint.options = [
|
this.vCheckpoint.options = [
|
||||||
{
|
{
|
||||||
code: "keys",
|
code: "keys",
|
||||||
@@ -181,6 +194,10 @@ Vue.component("http-firewall-checkpoint-cc", {
|
|||||||
{
|
{
|
||||||
code: "ignoreCommonFiles",
|
code: "ignoreCommonFiles",
|
||||||
value: ignoreCommonFiles
|
value: ignoreCommonFiles
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "enableFingerprint",
|
||||||
|
value: enableFingerprint
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -218,6 +235,13 @@ Vue.component("http-firewall-checkpoint-cc", {
|
|||||||
<p class="comment" v-if="thresholdTooLow()"><span class="red">对于网站类应用来说,当前阈值设置的太低,有可能会影响用户正常访问。</span></p>
|
<p class="comment" v-if="thresholdTooLow()"><span class="red">对于网站类应用来说,当前阈值设置的太低,有可能会影响用户正常访问。</span></p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>检查请求来源指纹</td>
|
||||||
|
<td>
|
||||||
|
<checkbox v-model="enableFingerprint"></checkbox>
|
||||||
|
<p class="comment">在接收到HTTPS请求时尝试检查请求来源的指纹,用来检测代理服务和爬虫攻击。</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>忽略常见文件</td>
|
<td>忽略常见文件</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -43,6 +43,12 @@ Vue.component("http-location-labels", {
|
|||||||
<!-- 反向代理 -->
|
<!-- 反向代理 -->
|
||||||
<http-location-labels-label v-if="refIsOn(location.reverseProxyRef, location.reverseProxy)" :v-href="url('/reverseProxy')">源站</http-location-labels-label>
|
<http-location-labels-label v-if="refIsOn(location.reverseProxyRef, location.reverseProxy)" :v-href="url('/reverseProxy')">源站</http-location-labels-label>
|
||||||
|
|
||||||
|
<!-- UAM -->
|
||||||
|
<http-location-labels-label v-if="location.web != null && location.web.uam != null && location.web.uam.isPrior"><span :class="{disabled: !location.web.uam.isOn, red:location.web.uam.isOn}">5秒盾</span></http-location-labels-label>
|
||||||
|
|
||||||
|
<!-- CC -->
|
||||||
|
<http-location-labels-label v-if="location.web != null && location.web.cc != null && location.web.cc.isPrior"><span :class="{disabled: !location.web.cc.isOn, red:location.web.cc.isOn}">CC防护</span></http-location-labels-label>
|
||||||
|
|
||||||
<!-- WAF -->
|
<!-- WAF -->
|
||||||
<!-- TODO -->
|
<!-- TODO -->
|
||||||
|
|
||||||
|
|||||||
@@ -27,8 +27,16 @@ Vue.component("http-referers-config-box", {
|
|||||||
return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn
|
return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn
|
||||||
},
|
},
|
||||||
changeAllowDomains: function (domains) {
|
changeAllowDomains: function (domains) {
|
||||||
|
if (typeof (domains) == "object") {
|
||||||
|
this.config.allowDomains = domains
|
||||||
|
this.$forceUpdate()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
changeDenyDomains: function (domains) {
|
changeDenyDomains: function (domains) {
|
||||||
|
if (typeof (domains) == "object") {
|
||||||
|
this.config.denyDomains = domains
|
||||||
|
this.$forceUpdate()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
template: `<div>
|
template: `<div>
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
// UAM模式配置
|
|
||||||
Vue.component("uam-config-box", {
|
|
||||||
props: ["v-uam-config", "v-is-location", "v-is-group"],
|
|
||||||
data: function () {
|
|
||||||
let config = this.vUamConfig
|
|
||||||
if (config == null) {
|
|
||||||
config = {
|
|
||||||
isPrior: false,
|
|
||||||
isOn: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
config: config
|
|
||||||
}
|
|
||||||
},
|
|
||||||
template: `<div>
|
|
||||||
<input type="hidden" name="uamJSON" :value="JSON.stringify(config)"/>
|
|
||||||
<table class="ui table definition selectable">
|
|
||||||
<prior-checkbox :v-config="config" v-if="vIsLocation || vIsGroup"></prior-checkbox>
|
|
||||||
<tbody v-show="((!vIsLocation && !vIsGroup) || config.isPrior)">
|
|
||||||
<tr>
|
|
||||||
<td class="title">启用5秒盾</td>
|
|
||||||
<td>
|
|
||||||
<checkbox v-model="config.isOn"></checkbox>
|
|
||||||
<p class="comment"><plus-label></plus-label>启用后,访问网站时,自动检查浏览器环境,阻止非正常访问。</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div class="margin"></div>
|
|
||||||
</div>`
|
|
||||||
})
|
|
||||||
@@ -334,7 +334,7 @@ window.teaweb = {
|
|||||||
popupTip: function (html) {
|
popupTip: function (html) {
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
html: '<div style="line-height: 1.7;text-align: left "><i class="icon question circle"></i>' + html + "</div>",
|
html: '<div style="line-height: 1.7;text-align: left "><i class="icon question circle"></i>' + html + "</div>",
|
||||||
width: "30em",
|
width: "34em",
|
||||||
padding: "4em",
|
padding: "4em",
|
||||||
showConfirmButton: false,
|
showConfirmButton: false,
|
||||||
showCloseButton: true,
|
showCloseButton: true,
|
||||||
@@ -991,7 +991,45 @@ window.teaweb = {
|
|||||||
}
|
}
|
||||||
return color
|
return color
|
||||||
},
|
},
|
||||||
DefaultChartColor: "#9DD3E8"
|
DefaultChartColor: "#9DD3E8",
|
||||||
|
validateIP: function (ip) {
|
||||||
|
if (typeof ip != "string") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ip.length == 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPv6
|
||||||
|
if (ip.indexOf(":") >= 0) {
|
||||||
|
let pieces = ip.split(":")
|
||||||
|
if (pieces.length > 8) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let isOk = true
|
||||||
|
pieces.forEach(function (piece) {
|
||||||
|
if (!/^[\da-fA-F]{0,4}$/.test(piece)) {
|
||||||
|
isOk = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return isOk
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ip.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let pieces = ip.split(".")
|
||||||
|
let isOk = true
|
||||||
|
pieces.forEach(function (v) {
|
||||||
|
let v1 = parseInt(v)
|
||||||
|
if (v1 > 255) {
|
||||||
|
isOk = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return isOk
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String.prototype.quoteIP = function () {
|
String.prototype.quoteIP = function () {
|
||||||
|
|||||||
@@ -7,5 +7,4 @@
|
|||||||
<a class="item" @click.prevent="showQQGroupQrcode()" title="点击弹出加群二维码">QQ讨论群 <i class="icon qrcode"></i> </a>
|
<a class="item" @click.prevent="showQQGroupQrcode()" title="点击弹出加群二维码">QQ讨论群 <i class="icon qrcode"></i> </a>
|
||||||
<a class="item" href="https://goedge.cn/community/telegram" target="_blank" title="点击跳转到加群页面">Telegram群 <i class="icon paper plane"></i></a>
|
<a class="item" href="https://goedge.cn/community/telegram" target="_blank" title="点击跳转到加群页面">Telegram群 <i class="icon paper plane"></i></a>
|
||||||
<a class="item right" href="https://goedge.cn/commercial" target="_blank" v-if="!teaIsPlus">企业版</a>
|
<a class="item right" href="https://goedge.cn/commercial" target="_blank" v-if="!teaIsPlus">企业版</a>
|
||||||
<a class="item right" href="https://goedge.cn/docs/Appendix/Donation.md" target="_blank" v-if="teaIsPlus">捐助作者</a>
|
|
||||||
</div>
|
</div>
|
||||||
@@ -23,7 +23,12 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tr v-for="node in nodes">
|
<tr v-for="node in nodes">
|
||||||
<td><a :href="'/api/node?nodeId=' + node.id">{{node.name}}</a>
|
<td><a :href="'/api/node?nodeId=' + node.id">{{node.name}}</a>
|
||||||
<grey-label v-if="node.isPrimary">主节点</grey-label>
|
<div v-if="node.isPrimary">
|
||||||
|
<grey-label v-if="node.isPrimary">主节点</grey-label>
|
||||||
|
</div>
|
||||||
|
<div v-if="node.status != null && node.status.shouldUpgrade">
|
||||||
|
<span class="red small">v{{node.status.buildVersion}} -> v{{node.status.latestVersion}}<br/><a href="" v-if="node.status.canUpgrade" @click.prevent="upgradeNode(node.id)">[远程升级]</a> </span>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div v-if="node.accessAddrs != null && node.accessAddrs.length > 0">
|
<div v-if="node.accessAddrs != null && node.accessAddrs.length > 0">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
Tea.context(function () {
|
Tea.context(function () {
|
||||||
// 创建节点
|
// 创建节点
|
||||||
this.createNode = function () {
|
this.createNode = function () {
|
||||||
teaweb.popup("/api/node/createPopup", {
|
teaweb.popup(".node.createPopup", {
|
||||||
width: "50em",
|
width: "50em",
|
||||||
height: "30em",
|
height: "30em",
|
||||||
callback: function () {
|
callback: function () {
|
||||||
@@ -16,11 +16,20 @@ Tea.context(function () {
|
|||||||
this.deleteNode = function (nodeId) {
|
this.deleteNode = function (nodeId) {
|
||||||
let that = this
|
let that = this
|
||||||
teaweb.confirm("确定要删除此节点吗?", function () {
|
teaweb.confirm("确定要删除此节点吗?", function () {
|
||||||
that.$post("/api/delete")
|
that.$post(".delete")
|
||||||
.params({
|
.params({
|
||||||
nodeId: nodeId
|
nodeId: nodeId
|
||||||
})
|
})
|
||||||
.refresh()
|
.refresh()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 升级节点
|
||||||
|
this.upgradeNode = function (nodeId) {
|
||||||
|
teaweb.popup(".node.upgradePopup?nodeId=" + nodeId, {
|
||||||
|
onClose: function () {
|
||||||
|
teaweb.reload()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
35
web/views/@default/api/node/upgradePopup.html
Normal file
35
web/views/@default/api/node/upgradePopup.html
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{$layout "layout_popup"}
|
||||||
|
|
||||||
|
<h3>远程升级</h3>
|
||||||
|
|
||||||
|
<form class="ui form" data-tea-action="$" data-tea-success="success" data-tea-timeout="3600" data-tea-before="before" data-tea-done="done">
|
||||||
|
<csrf-token></csrf-token>
|
||||||
|
<input type="hidden" name="nodeId" :value="nodeId"/>
|
||||||
|
|
||||||
|
<table class="ui table definition selectable">
|
||||||
|
<tr v-show="nodeName.length > 0">
|
||||||
|
<td class="title">API节点</td>
|
||||||
|
<td>{{nodeName}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-show="currentVersion.length > 0">
|
||||||
|
<td>当前版本</td>
|
||||||
|
<td>v{{currentVersion}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-show="latestVersion.length > 0">
|
||||||
|
<td>目标版本</td>
|
||||||
|
<td>v{{latestVersion}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="title">升级结果</td>
|
||||||
|
<td>
|
||||||
|
<span v-if="currentVersion == latestVersion" class="green">已经升级到最新版本</span>
|
||||||
|
<span :class="{red: !resultIsOk}" v-if="currentVersion != latestVersion">
|
||||||
|
<span v-if="!isRequesting">{{result}}</span>
|
||||||
|
<span v-if="isRequesting">升级中...</span>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<submit-btn v-show="canUpgrade && !isRequesting && !isUpgrading">开始升级</submit-btn>
|
||||||
|
</form>
|
||||||
39
web/views/@default/api/node/upgradePopup.js
Normal file
39
web/views/@default/api/node/upgradePopup.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
Tea.context(function () {
|
||||||
|
this.$delay(function () {
|
||||||
|
this.checkLoop()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.success = function () {
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isRequesting = false
|
||||||
|
|
||||||
|
this.before = function () {
|
||||||
|
this.isRequesting = true
|
||||||
|
}
|
||||||
|
|
||||||
|
this.done = function () {
|
||||||
|
this.isRequesting = false
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkLoop = function () {
|
||||||
|
if (this.currentVersion == this.latestVersion) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$post(".upgradeCheck")
|
||||||
|
.params({
|
||||||
|
nodeId: this.nodeId
|
||||||
|
})
|
||||||
|
.success(function (resp) {
|
||||||
|
if (resp.data.isOk) {
|
||||||
|
teaweb.reload()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.done(function () {
|
||||||
|
this.$delay(function () {
|
||||||
|
this.checkLoop()
|
||||||
|
}, 3000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<div v-if="node.ipAddresses.length > 0">
|
<div v-if="node.ipAddresses.length > 0">
|
||||||
<div>
|
<div>
|
||||||
<div v-for="(address, index) in node.ipAddresses" class="ui label tiny basic">
|
<div v-for="(address, index) in node.ipAddresses" class="ui label small basic">
|
||||||
<span v-if="address.ip.indexOf(':') > -1" class="grey">[IPv6]</span> {{address.ip}}
|
<span v-if="address.ip.indexOf(':') > -1" class="grey">[IPv6]</span> {{address.ip}}
|
||||||
<span class="small red" v-if="address.originIP != null && address.originIP.length > 0 && address.originIP != address.ip">(原:{{address.originIP}})</span>
|
<span class="small red" v-if="address.originIP != null && address.originIP.length > 0 && address.originIP != address.ip">(原:{{address.originIP}})</span>
|
||||||
<span class="small grey" v-if="address.name.length > 0">({{address.name}}<span v-if="!address.canAccess">,不可访问</span>)</span>
|
<span class="small grey" v-if="address.name.length > 0">({{address.name}}<span v-if="!address.canAccess">,不可访问</span>)</span>
|
||||||
@@ -30,6 +30,10 @@
|
|||||||
<span class="small red" v-if="!address.isOn">[off]</span>
|
<span class="small red" v-if="!address.isOn">[off]</span>
|
||||||
<span class="small red" v-if="!address.isUp">[down]</span>
|
<span class="small red" v-if="!address.isUp">[down]</span>
|
||||||
<span class="small" v-if="address.thresholds != null && address.thresholds.length > 0">[阈值]</span>
|
<span class="small" v-if="address.thresholds != null && address.thresholds.length > 0">[阈值]</span>
|
||||||
|
|
||||||
|
<span v-if="address.clusters.length > 0">
|
||||||
|
<span class="small grey">专属集群:[</span><span v-for="(cluster, index) in address.clusters" class="small grey">{{cluster.name}}<span v-if="index < address.clusters.length - 1">,</span></span><span class="small grey">]</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,6 +55,7 @@
|
|||||||
<table class="ui table celled">
|
<table class="ui table celled">
|
||||||
<thead class="full-width">
|
<thead class="full-width">
|
||||||
<tr>
|
<tr>
|
||||||
|
<th>集群</th>
|
||||||
<th>记录名</th>
|
<th>记录名</th>
|
||||||
<th>记录类型</th>
|
<th>记录类型</th>
|
||||||
<th>线路</th>
|
<th>线路</th>
|
||||||
@@ -59,6 +64,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tr v-for="record in node.records">
|
<tr v-for="record in node.records">
|
||||||
|
<td>{{record.clusterName}}</td>
|
||||||
<td>{{record.name}}</td>
|
<td>{{record.name}}</td>
|
||||||
<td>{{record.type}}</td>
|
<td>{{record.type}}</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>IP地址 *</td>
|
<td>IP地址 *</td>
|
||||||
<td>
|
<td>
|
||||||
<node-ip-addresses-box :v-ip-addresses="ipAddresses"></node-ip-addresses-box>
|
<node-ip-addresses-box :v-ip-addresses="ipAddresses" :v-node-id="node.id"></node-ip-addresses-box>
|
||||||
<p class="comment">用于访问节点和域名解析等。</p>
|
<p class="comment">用于访问节点和域名解析等。</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -94,6 +94,11 @@
|
|||||||
<div class="ui label tiny basic">{{addr.ip}}
|
<div class="ui label tiny basic">{{addr.ip}}
|
||||||
<span class="small" v-if="addr.name.length > 0">({{addr.name}}<span v-if="!addr.canAccess">,不可访问</span>)</span>
|
<span class="small" v-if="addr.name.length > 0">({{addr.name}}<span v-if="!addr.canAccess">,不可访问</span>)</span>
|
||||||
<span class="small" v-if="addr.name.length == 0 && !addr.canAccess">(不可访问)</span>
|
<span class="small" v-if="addr.name.length == 0 && !addr.canAccess">(不可访问)</span>
|
||||||
|
|
||||||
|
<!-- 专属集群 -->
|
||||||
|
<div v-if="addr.clusters != null && addr.clusters.length > 0" style="margin-top: 0.3em; font-weight: normal">
|
||||||
|
<span class="small grey">[</span><span v-for="(cluster, index) in addr.clusters" class="small grey">{{cluster.name}}<span v-if="index < addr.clusters.length - 1">,</span></span><span class="small grey">]</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -77,6 +77,13 @@
|
|||||||
<p class="comment">选中后,表示访问日志中记录Cookie内容。</p>
|
<p class="comment">选中后,表示访问日志中记录Cookie内容。</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>记录找不到网站日志</td>
|
||||||
|
<td>
|
||||||
|
<checkbox name="httpAccessLogEnableServerNotFound" v-model="config.httpAccessLog.enableServerNotFound"></checkbox>
|
||||||
|
<p class="comment">选中后,表示如果访客访问的域名对应的网站不存在也会记录日志。</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h4>运行日志</h4>
|
<h4>运行日志</h4>
|
||||||
|
|||||||
@@ -2,4 +2,11 @@ pre.log-box {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
.search-keyword-label a {
|
||||||
|
opacity: 1!important;
|
||||||
|
}
|
||||||
|
.search-keyword-label span.small {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #4183c4 !important;
|
||||||
|
}
|
||||||
/*# sourceMappingURL=index.css.map */
|
/*# sourceMappingURL=index.css.map */
|
||||||
@@ -1 +1 @@
|
|||||||
{"version":3,"sources":["index.less"],"names":[],"mappings":"AAAA,GAAG;EACF,SAAA;EACA,UAAA","file":"index.css"}
|
{"version":3,"sources":["index.less"],"names":[],"mappings":"AAAA,GAAG;EACF,SAAA;EACA,UAAA;;AAGD,qBACC;EACC,oBAAA;;AAFF,qBAKC,KAAI;EACH,gBAAA;EACA,cAAA","file":"index.css"}
|
||||||
@@ -68,6 +68,10 @@
|
|||||||
|
|
||||||
<p class="comment" v-if="logs.length == 0">暂时还没有<span v-if="type == 'unread'">未读</span><span v-if="type == 'needFix'">需修复</span>日志。</p>
|
<p class="comment" v-if="logs.length == 0">暂时还没有<span v-if="type == 'unread'">未读</span><span v-if="type == 'needFix'">需修复</span>日志。</p>
|
||||||
|
|
||||||
|
<div v-if="countLogs > 0 && searchedKeyword.length > 0" class="search-keyword-label">
|
||||||
|
<div class="ui label basic small">正在搜索关键词"{{searchedKeyword}}",共{{countLogs}}条记录 <a href="" @click.prevent="deleteLogs"><span class="small">[清除日志]</span></a> </div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<table class="ui table selectable celled" v-if="logs.length > 0">
|
<table class="ui table selectable celled" v-if="logs.length > 0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -82,4 +82,23 @@ Tea.context(function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.deleteLogs = function () {
|
||||||
|
teaweb.confirm("确定要删除当前关键词\"" + this.searchedKeyword + "\"匹配的" + this.countLogs + "个运行日志?", function () {
|
||||||
|
this.$post(".deleteAll")
|
||||||
|
.params({
|
||||||
|
dayFrom: this.dayFrom,
|
||||||
|
dayTo: this.dayTo,
|
||||||
|
keyword: this.keyword,
|
||||||
|
level: this.level,
|
||||||
|
type: this.type,
|
||||||
|
tag: this.tag,
|
||||||
|
clusterId: this.clusterId,
|
||||||
|
nodeId: this.nodeId
|
||||||
|
})
|
||||||
|
.success(function () {
|
||||||
|
teaweb.reload()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
@@ -1,4 +1,15 @@
|
|||||||
pre.log-box {
|
pre.log-box {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-keyword-label {
|
||||||
|
a {
|
||||||
|
opacity: 1!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.small {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #4183c4!important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -88,6 +88,11 @@
|
|||||||
<div class="ui label tiny basic">{{addr.ip}}
|
<div class="ui label tiny basic">{{addr.ip}}
|
||||||
<span class="small" v-if="addr.name.length > 0">({{addr.name}}<span v-if="!addr.canAccess">,不可访问</span>)</span>
|
<span class="small" v-if="addr.name.length > 0">({{addr.name}}<span v-if="!addr.canAccess">,不可访问</span>)</span>
|
||||||
<span class="small" v-if="addr.name.length == 0 && !addr.canAccess">(不可访问)</span>
|
<span class="small" v-if="addr.name.length == 0 && !addr.canAccess">(不可访问)</span>
|
||||||
|
|
||||||
|
<!-- 专属集群 -->
|
||||||
|
<div v-if="addr.clusters != null && addr.clusters.length > 0" style="margin-top: 0.3em; font-weight: normal">
|
||||||
|
<span class="small grey">[</span><span v-for="(cluster, index) in addr.clusters" class="small grey">{{cluster.name}}<span v-if="index < addr.clusters.length - 1">,</span></span><span class="small grey">]</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -41,12 +41,6 @@
|
|||||||
<input type="password" v-model="password" placeholder="请输入密码" maxlength="200" @input="changePassword()" ref="passwordRef"/>
|
<input type="password" v-model="password" placeholder="请输入密码" maxlength="200" @input="changePassword()" ref="passwordRef"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui field" v-show="showOTP">
|
|
||||||
<div class="ui left icon input">
|
|
||||||
<i class="ui barcode icon"></i>
|
|
||||||
<input type="text" name="otpCode" placeholder="请输入OTP动态密码" maxlength="6"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ui field" v-if="rememberLogin">
|
<div class="ui field" v-if="rememberLogin">
|
||||||
<a href="" @click.prevent="showMoreOptions()">更多选项 <i class="icon angle" :class="{down:!moreOptionsVisible, up:moreOptionsVisible}"></i> </a>
|
<a href="" @click.prevent="showMoreOptions()">更多选项 <i class="icon angle" :class="{down:!moreOptionsVisible, up:moreOptionsVisible}"></i> </a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ Tea.context(function () {
|
|||||||
this.password = "123456"
|
this.password = "123456"
|
||||||
}
|
}
|
||||||
|
|
||||||
this.showOTP = false
|
|
||||||
|
|
||||||
this.isSubmitting = false
|
this.isSubmitting = false
|
||||||
|
|
||||||
this.$delay(function () {
|
this.$delay(function () {
|
||||||
@@ -19,13 +17,7 @@ Tea.context(function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.changeUsername = function () {
|
this.changeUsername = function () {
|
||||||
this.$post("/checkOTP")
|
|
||||||
.params({
|
|
||||||
username: this.username
|
|
||||||
})
|
|
||||||
.success(function (resp) {
|
|
||||||
this.showOTP = resp.data.requireOTP
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.changePassword = function () {
|
this.changePassword = function () {
|
||||||
@@ -46,7 +38,11 @@ Tea.context(function () {
|
|||||||
this.isSubmitting = false;
|
this.isSubmitting = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.submitSuccess = function () {
|
this.submitSuccess = function (resp) {
|
||||||
|
if (resp.data.requireOTP) {
|
||||||
|
window.location = "/index/otp?sid=" + resp.data.sid + "&remember=" + (resp.data.remember ? 1 : 0) + "&from=" + window.encodeURIComponent(this.from)
|
||||||
|
return
|
||||||
|
}
|
||||||
if (this.from.length == 0) {
|
if (this.from.length == 0) {
|
||||||
window.location = "/dashboard";
|
window.location = "/dashboard";
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
42
web/views/@default/index/otp.css
Normal file
42
web/views/@default/index/otp.css
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
.form-box {
|
||||||
|
position: fixed;
|
||||||
|
top: 2em;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
form {
|
||||||
|
position: fixed;
|
||||||
|
width: 21em;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -10em;
|
||||||
|
margin-top: -16em;
|
||||||
|
}
|
||||||
|
form .header {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1em !important;
|
||||||
|
}
|
||||||
|
form p {
|
||||||
|
font-size: 0.8em;
|
||||||
|
margin-top: 0.3em;
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
form .comment {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
padding: 0.5em;
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
form .cancel-login {
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 1em;
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 512px) {
|
||||||
|
form {
|
||||||
|
width: 80%;
|
||||||
|
margin-left: -40%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*# sourceMappingURL=otp.css.map */
|
||||||
1
web/views/@default/index/otp.css.map
Normal file
1
web/views/@default/index/otp.css.map
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"sources":["otp.less"],"names":[],"mappings":"AAAA;EACI,eAAA;EACA,QAAA;EACA,SAAA;EACA,OAAA;EACA,QAAA;;AAGJ;EACI,eAAA;EACA,WAAA;EACA,QAAA;EACA,SAAA;EACA,kBAAA;EACA,iBAAA;;AANJ,IAQC;EACC,kBAAA;EACA,yBAAA;;AAVF,IAaC;EACC,gBAAA;EACA,iBAAA;EACA,gBAAA;EACA,mBAAA;EACA,UAAA;;AAlBF,IAqBC;EACC,iBAAA;EACA,cAAA;EACA,WAAA;;AAxBF,IA2BC;EACC,kBAAA;EACA,gBAAA;;AAIF,mBAAqC;EACjC;IACI,UAAA;IACA,iBAAA","file":"otp.css"}
|
||||||
55
web/views/@default/index/otp.html
Normal file
55
web/views/@default/index/otp.html
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||||
|
{$if eq .faviconFileId 0}
|
||||||
|
<link rel="shortcut icon" href="/images/favicon.png"/>
|
||||||
|
{$else}
|
||||||
|
<link rel="shortcut icon" href="/ui/image/{$ .faviconFileId}"/>
|
||||||
|
{$end}
|
||||||
|
<title>登录{$.systemName} - 二次验证</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
|
||||||
|
{$TEA.VUE}
|
||||||
|
{$TEA.SEMANTIC}
|
||||||
|
<script type="text/javascript" src="/js/md5.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/js/utils.js"></script>
|
||||||
|
<script type="text/javascript" src="/js/sweetalert2/dist/sweetalert2.all.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/js/components.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
{$template "/menu"}
|
||||||
|
|
||||||
|
<div class="form-box">
|
||||||
|
<form method="post" class="ui form" data-tea-action="$" data-tea-before="submitBefore"
|
||||||
|
data-tea-done="submitDone" data-tea-success="submitSuccess" autocomplete="off">
|
||||||
|
<input type="hidden" name="sid" :value="sid"/>
|
||||||
|
<input type="hidden" name="remember" :value="remember ? 1 : 0"/>
|
||||||
|
<div class="ui segment stacked">
|
||||||
|
<div class="ui header">
|
||||||
|
登录{$.systemName}
|
||||||
|
</div>
|
||||||
|
<div class="ui field">
|
||||||
|
为了保护你的账户安全,需要进行OTP二次身份验证。
|
||||||
|
</div>
|
||||||
|
<div class="ui field">
|
||||||
|
<div class="ui left icon input">
|
||||||
|
<i class="ui barcode icon"></i>
|
||||||
|
<input type="text" name="otpCode" placeholder="请输入OTP动态密码" maxlength="6"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="ui button primary fluid" type="submit" v-if="!isSubmitting">验证</button>
|
||||||
|
<button class="ui button primary fluid disabled" type="submit" v-if="isSubmitting">验证中...</button>
|
||||||
|
|
||||||
|
<div class="ui field cancel-login">
|
||||||
|
<a :href="'/?from=' + encodedFrom">« 取消登录</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
31
web/views/@default/index/otp.js
Normal file
31
web/views/@default/index/otp.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
Tea.context(function () {
|
||||||
|
this.isSubmitting = false
|
||||||
|
|
||||||
|
this.encodedFrom = window.encodeURIComponent(this.from)
|
||||||
|
|
||||||
|
this.$delay(function () {
|
||||||
|
this.$find("form input[name='otpCode']").focus()
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更多选项
|
||||||
|
this.moreOptionsVisible = false;
|
||||||
|
this.showMoreOptions = function () {
|
||||||
|
this.moreOptionsVisible = !this.moreOptionsVisible;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.submitBefore = function () {
|
||||||
|
this.isSubmitting = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.submitDone = function () {
|
||||||
|
this.isSubmitting = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.submitSuccess = function (resp) {
|
||||||
|
if (this.from.length == 0) {
|
||||||
|
window.location = "/dashboard";
|
||||||
|
} else {
|
||||||
|
window.location = this.from;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
47
web/views/@default/index/otp.less
Normal file
47
web/views/@default/index/otp.less
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
.form-box {
|
||||||
|
position: fixed;
|
||||||
|
top: 2em;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
position: fixed;
|
||||||
|
width: 21em;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -10em;
|
||||||
|
margin-top: -16em;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 0.8em;
|
||||||
|
margin-top: 0.3em;
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
padding: 0.5em;
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-login {
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 512px) {
|
||||||
|
form {
|
||||||
|
width: 80%;
|
||||||
|
margin-left: -40%;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user