Compare commits
60 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 | ||
|
|
5e62769dcf | ||
|
|
86b8a718a0 | ||
|
|
a729cfc31d | ||
|
|
96cfda852a | ||
|
|
0423d9246c | ||
|
|
985798757f | ||
|
|
72876f6749 |
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/apps"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/configs"
|
||||
@@ -24,7 +25,7 @@ func main() {
|
||||
var app = apps.NewAppCmd().
|
||||
Version(teaconst.Version).
|
||||
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]").
|
||||
Option("-h", "show this help").
|
||||
Option("-v", "show version").
|
||||
@@ -38,7 +39,7 @@ func main() {
|
||||
Option("demo", "switch to demo mode").
|
||||
Option("dev", "switch to 'dev' 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() {
|
||||
nodes.NewAdminNode().Daemon()
|
||||
@@ -138,7 +139,12 @@ func main() {
|
||||
}
|
||||
})
|
||||
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 ...")
|
||||
var ticker = time.NewTicker(1 * time.Second)
|
||||
go func() {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
FROM alpine:latest
|
||||
LABEL maintainer="iwind.liu@gmail.com"
|
||||
ENV TZ "Asia/Shanghai"
|
||||
ENV VERSION 0.5.9
|
||||
ENV VERSION 0.6.3
|
||||
ENV ROOT_DIR /usr/local/goedge
|
||||
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 "http://192.168.2.60:8080/edge-admin-linux-amd64-plus-v${VERSION}.zip" # your local repository
|
||||
|
||||
RUN apk add --no-cache tzdata
|
||||
|
||||
|
||||
11
go.mod
11
go.mod
@@ -8,14 +8,14 @@ require (
|
||||
github.com/TeaOSLab/EdgeCommon v0.0.0-00010101000000-000000000000
|
||||
github.com/cespare/xxhash v1.1.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/miekg/dns v1.1.43
|
||||
github.com/shirou/gopsutil/v3 v3.22.5
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/tealeg/xlsx/v3 v3.2.3
|
||||
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
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
@@ -26,18 +26,15 @@ require (
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/btree v1.0.0 // 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/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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/rogpeppe/fastuuid v1.2.0 // indirect
|
||||
github.com/shabbyrobe/xmlwriter v0.0.0-20200208144257-9fca06d00ffa // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||
golang.org/x/net v0.2.0 // indirect
|
||||
golang.org/x/text v0.4.0 // indirect
|
||||
golang.org/x/net v0.7.0 // indirect
|
||||
golang.org/x/text v0.7.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220317150908-0efb43f6373e // indirect
|
||||
google.golang.org/protobuf v1.27.1 // 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/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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
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/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/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-20220304043459-0dd944a5b475 h1:EseyfFaQOjWanGiby9KMw7PjDBMg/95tLDgIw/ns0Cw=
|
||||
github.com/iwind/TeaGo v0.0.0-20220304043459-0dd944a5b475/go.mod h1:HRHK0zoC/og3c9/hKosD9yYVMTnnzm3PgXUdhRYHaLc=
|
||||
github.com/iwind/TeaGo v0.0.0-20230207032553-d6dcde0cd518 h1:zuWjQ57zc67ZSTHpxNK95JYoa9Ph/JRSnapsTY/hlhQ=
|
||||
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/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.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.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
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/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-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 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/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
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/pkg/profile v1.5.0 h1:042Buzk+NhDI+DeSAA62RwJL8VAuZUMQZUjCsRz1Mug=
|
||||
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/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=
|
||||
@@ -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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||
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/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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
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.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||
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-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
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-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.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||
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/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.3/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.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||
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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
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.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-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
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))
|
||||
}
|
||||
|
||||
// 重启
|
||||
// RunRestart 重启
|
||||
func (this *AppCmd) RunRestart() {
|
||||
this.runStop()
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
@@ -155,3 +155,15 @@ func (this *APIConfig) WriteFile(path string) error {
|
||||
|
||||
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
|
||||
|
||||
const (
|
||||
Version = "0.6.0"
|
||||
Version = "0.6.4"
|
||||
|
||||
APINodeVersion = "0.6.0"
|
||||
APINodeVersion = "0.6.4"
|
||||
|
||||
ProductName = "Edge Admin"
|
||||
ProcessName = "edge-admin"
|
||||
@@ -18,5 +18,5 @@ const (
|
||||
CookieSID = "edgesid"
|
||||
|
||||
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/maps"
|
||||
"github.com/iwind/TeaGo/rands"
|
||||
"github.com/iwind/TeaGo/sessions"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"github.com/iwind/gosock/pkg/gosock"
|
||||
"gopkg.in/yaml.v3"
|
||||
@@ -21,6 +20,8 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
@@ -85,10 +86,15 @@ func (this *AdminNode) Run() {
|
||||
this.startAPINode()
|
||||
|
||||
// 启动Web服务
|
||||
sessionManager, err := NewSessionManager()
|
||||
if err != nil {
|
||||
log.Fatal("start session failed: " + err.Error())
|
||||
return
|
||||
}
|
||||
TeaGo.NewServer(false).
|
||||
AccessLog(false).
|
||||
EndAll().
|
||||
Session(sessions.NewFileSessionManager(86400, secret), teaconst.CookieSID).
|
||||
Session(sessionManager, teaconst.CookieSID).
|
||||
ReadHeaderTimeout(3 * time.Second).
|
||||
ReadTimeout(1200 * time.Second).
|
||||
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)
|
||||
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())
|
||||
}
|
||||
|
||||
func (this *RPCClient) LoginSessionRPC() pb.LoginSessionServiceClient {
|
||||
return pb.NewLoginSessionServiceClient(this.pickConn())
|
||||
}
|
||||
|
||||
func (this *RPCClient) NodeTaskRPC() pb.NodeTaskServiceClient {
|
||||
return pb.NewNodeTaskServiceClient(this.pickConn())
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ func (this *CheckUpdatesTask) Loop() error {
|
||||
var apiURL = teaconst.UpdatesURL
|
||||
apiURL = strings.ReplaceAll(apiURL, "${os}", runtime.GOOS)
|
||||
apiURL = strings.ReplaceAll(apiURL, "${arch}", runtime.GOARCH)
|
||||
apiURL = strings.ReplaceAll(apiURL, "${version}", teaconst.Version)
|
||||
resp, err := http.Get(apiURL)
|
||||
if err != nil {
|
||||
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
|
||||
body io.ReadCloser
|
||||
isCancelled bool
|
||||
|
||||
downloadURL string
|
||||
}
|
||||
|
||||
func NewUpgradeManager(component string) *UpgradeManager {
|
||||
func NewUpgradeManager(component string, downloadURL string) *UpgradeManager {
|
||||
return &UpgradeManager{
|
||||
component: component,
|
||||
component: component,
|
||||
downloadURL: downloadURL,
|
||||
client: &http.Client{
|
||||
Transport: &http.Transport{
|
||||
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 osName = runtime.GOOS
|
||||
if Tea.IsTesting() && osName == "darwin" {
|
||||
@@ -105,10 +108,12 @@ func (this *UpgradeManager) Start() error {
|
||||
}
|
||||
url = strings.ReplaceAll(url, "${os}", osName)
|
||||
url = strings.ReplaceAll(url, "${arch}", runtime.GOARCH)
|
||||
url = strings.ReplaceAll(url, "${version}", teaconst.Version)
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return errors.New("create url request failed: " + err.Error())
|
||||
}
|
||||
req.Header.Set("User-Agent", "Edge-Admin/"+teaconst.Version)
|
||||
|
||||
resp, err := this.client.Do(req)
|
||||
if err != nil {
|
||||
@@ -169,6 +174,7 @@ func (this *UpgradeManager) Start() error {
|
||||
if err != nil {
|
||||
return errors.New("create download request failed: " + err.Error())
|
||||
}
|
||||
req.Header.Set("User-Agent", "Edge-Admin/"+teaconst.Version)
|
||||
|
||||
resp, err := this.client.Do(req)
|
||||
if err != nil {
|
||||
|
||||
@@ -3,12 +3,15 @@ package api
|
||||
import (
|
||||
"encoding/json"
|
||||
"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/EdgeCommon/pkg/nodeconfigs"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
stringutil "github.com/iwind/TeaGo/utils/string"
|
||||
timeutil "github.com/iwind/TeaGo/utils/time"
|
||||
"time"
|
||||
)
|
||||
@@ -44,7 +47,7 @@ func (this *IndexAction) RunGet(params struct{}) {
|
||||
|
||||
for _, node := range nodesResp.ApiNodes {
|
||||
// 状态
|
||||
status := &nodeconfigs.NodeStatus{}
|
||||
var status = &nodeconfigs.NodeStatus{}
|
||||
if len(node.StatusJSON) > 0 {
|
||||
err = json.Unmarshal(node.StatusJSON, &status)
|
||||
if err != nil {
|
||||
@@ -55,7 +58,7 @@ func (this *IndexAction) RunGet(params struct{}) {
|
||||
}
|
||||
|
||||
// Rest地址
|
||||
restAccessAddrs := []string{}
|
||||
var restAccessAddrs = []string{}
|
||||
if node.RestIsOn {
|
||||
if len(node.RestHTTPJSON) > 0 {
|
||||
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{
|
||||
"id": node.Id,
|
||||
"isOn": node.IsOn,
|
||||
@@ -94,14 +100,17 @@ func (this *IndexAction) RunGet(params struct{}) {
|
||||
"restAccessAddrs": restAccessAddrs,
|
||||
"isPrimary": node.IsPrimary,
|
||||
"status": maps.Map{
|
||||
"isActive": status.IsActive,
|
||||
"updatedAt": status.UpdatedAt,
|
||||
"hostname": status.Hostname,
|
||||
"cpuUsage": status.CPUUsage,
|
||||
"cpuUsageText": fmt.Sprintf("%.2f%%", status.CPUUsage*100),
|
||||
"memUsage": status.MemoryUsage,
|
||||
"memUsageText": fmt.Sprintf("%.2f%%", status.MemoryUsage*100),
|
||||
"buildVersion": status.BuildVersion,
|
||||
"isActive": status.IsActive,
|
||||
"updatedAt": status.UpdatedAt,
|
||||
"hostname": status.Hostname,
|
||||
"cpuUsage": status.CPUUsage,
|
||||
"cpuUsageText": fmt.Sprintf("%.2f%%", status.CPUUsage*100),
|
||||
"memUsage": status.MemoryUsage,
|
||||
"memUsageText": fmt.Sprintf("%.2f%%", status.MemoryUsage*100),
|
||||
"buildVersion": status.BuildVersion,
|
||||
"latestVersion": teaconst.APINodeVersion,
|
||||
"shouldUpgrade": shouldUpgrade,
|
||||
"canUpgrade": shouldUpgrade && canUpgrade,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -24,7 +24,10 @@ func init() {
|
||||
GetPost("/update", new(UpdateAction)).
|
||||
Get("/install", new(InstallAction)).
|
||||
Get("/logs", new(LogsAction)).
|
||||
GetPost("/upgradePopup", new(UpgradePopupAction)).
|
||||
Post("/upgradeCheck", new(UpgradeCheckAction)).
|
||||
|
||||
//
|
||||
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
|
||||
}
|
||||
var ipAddresses = ipAddressesResp.NodeIPAddresses
|
||||
ipAddressMaps := []maps.Map{}
|
||||
var ipAddressMaps = []maps.Map{}
|
||||
for _, addr := range ipAddressesResp.NodeIPAddresses {
|
||||
thresholds, err := ipaddressutils.InitNodeIPAddressThresholds(this.Parent(), addr.Id)
|
||||
if err != nil {
|
||||
@@ -103,6 +103,15 @@ func (this *DetailAction) RunGet(params struct {
|
||||
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{
|
||||
"id": addr.Id,
|
||||
"name": addr.Name,
|
||||
@@ -111,6 +120,7 @@ func (this *DetailAction) RunGet(params struct {
|
||||
"canAccess": addr.CanAccess,
|
||||
"isOn": addr.IsOn,
|
||||
"isUp": addr.IsUp,
|
||||
"clusters": addrClusterMaps,
|
||||
"thresholds": thresholds,
|
||||
})
|
||||
}
|
||||
@@ -152,16 +162,31 @@ func (this *DetailAction) RunGet(params struct {
|
||||
if !addr.CanAccess || !addr.IsUp || !addr.IsOn {
|
||||
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 {
|
||||
var recordType = "A"
|
||||
if utils.IsIPv6(addr.Ip) {
|
||||
recordType = "AAAA"
|
||||
}
|
||||
recordMaps = append(recordMaps, maps.Map{
|
||||
"name": dnsInfo.NodeClusterDNSName + "." + domainName,
|
||||
"type": recordType,
|
||||
"route": route.Name,
|
||||
"value": addr.Ip,
|
||||
"name": dnsInfo.NodeClusterDNSName + "." + domainName,
|
||||
"type": recordType,
|
||||
"route": route.Name,
|
||||
"value": addr.Ip,
|
||||
"clusterName": cluster.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -179,8 +204,8 @@ func (this *DetailAction) RunGet(params struct {
|
||||
}
|
||||
}
|
||||
|
||||
grantMap := maps.Map{}
|
||||
grantId := loginParams.GetInt64("grantId")
|
||||
var grantMap = maps.Map{}
|
||||
var grantId = loginParams.GetInt64("grantId")
|
||||
if grantId > 0 {
|
||||
grantResp, err := this.RPC().NodeGrantRPC().FindEnabledNodeGrant(this.AdminContext(), &pb.FindEnabledNodeGrantRequest{NodeGrantId: grantId})
|
||||
if err != nil {
|
||||
|
||||
@@ -64,12 +64,22 @@ func (this *UpdateAction) RunGet(params struct {
|
||||
}
|
||||
var ipAddressMaps = []maps.Map{}
|
||||
for _, addr := range ipAddressesResp.NodeIPAddresses {
|
||||
// 阈值
|
||||
thresholds, err := ipaddressutils.InitNodeIPAddressThresholds(this.Parent(), addr.Id)
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
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{
|
||||
"id": addr.Id,
|
||||
"name": addr.Name,
|
||||
@@ -78,6 +88,7 @@ func (this *UpdateAction) RunGet(params struct {
|
||||
"isOn": addr.IsOn,
|
||||
"isUp": addr.IsUp,
|
||||
"thresholds": thresholds,
|
||||
"clusters": clusterMaps,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -142,8 +142,17 @@ func (this *NodesAction) RunGet(params struct {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
ipAddresses := []maps.Map{}
|
||||
var ipAddresses = []maps.Map{}
|
||||
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{
|
||||
"id": addr.Id,
|
||||
"name": addr.Name,
|
||||
@@ -151,6 +160,7 @@ func (this *NodesAction) RunGet(params struct {
|
||||
"canAccess": addr.CanAccess,
|
||||
"isUp": addr.IsUp,
|
||||
"isOn": addr.IsOn,
|
||||
"clusters": addrClusterMaps,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ func (this *IndexAction) RunPost(params struct {
|
||||
HttpAccessLogEnableResponseHeaders bool
|
||||
HttpAccessLogCommonRequestHeadersOnly bool
|
||||
HttpAccessLogEnableCookies bool
|
||||
HttpAccessLogEnableServerNotFound bool
|
||||
|
||||
LogRecordServerError bool
|
||||
|
||||
@@ -138,6 +139,7 @@ func (this *IndexAction) RunPost(params struct {
|
||||
config.HTTPAccessLog.EnableResponseHeaders = params.HttpAccessLogEnableResponseHeaders
|
||||
config.HTTPAccessLog.CommonRequestHeadersOnly = params.HttpAccessLogCommonRequestHeadersOnly
|
||||
config.HTTPAccessLog.EnableCookies = params.HttpAccessLogEnableCookies
|
||||
config.HTTPAccessLog.EnableServerNotFound = params.HttpAccessLogEnableServerNotFound
|
||||
|
||||
// 日志
|
||||
config.Log.RecordServerError = params.LogRecordServerError
|
||||
|
||||
@@ -68,7 +68,7 @@ func (this *ClusterHelper) BeforeAction(actionPtr actions.ActionWrapper) (goNext
|
||||
actionutils.SetTabbar(action, tabbar)
|
||||
|
||||
// 左侧菜单
|
||||
secondMenuItem := action.Data.GetString("secondMenuItem")
|
||||
var secondMenuItem = action.Data.GetString("secondMenuItem")
|
||||
switch selectedTabbar {
|
||||
case "setting":
|
||||
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["dayTo"] = params.DayTo
|
||||
this.Data["keyword"] = params.Keyword
|
||||
this.Data["searchedKeyword"] = params.Keyword
|
||||
this.Data["level"] = params.Level
|
||||
this.Data["type"] = params.Type
|
||||
this.Data["tag"] = params.Tag
|
||||
@@ -97,6 +98,8 @@ func (this *IndexAction) RunGet(params struct {
|
||||
return
|
||||
}
|
||||
var count = countResp.Count
|
||||
this.Data["countLogs"] = count
|
||||
|
||||
var page = this.NewPage(count)
|
||||
this.Data["page"] = page.AsHTML()
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ func init() {
|
||||
Post("/readAllLogs", new(ReadAllLogsAction)).
|
||||
Post("/fix", new(FixAction)).
|
||||
Post("/fixAll", new(FixAllAction)).
|
||||
Post("/deleteAll", new(DeleteAllAction)).
|
||||
EndAll()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -144,8 +144,17 @@ func (this *NodesAction) RunGet(params struct {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
ipAddresses := []maps.Map{}
|
||||
var ipAddresses = []maps.Map{}
|
||||
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{
|
||||
"id": addr.Id,
|
||||
"name": addr.Name,
|
||||
@@ -153,6 +162,7 @@ func (this *NodesAction) RunGet(params struct {
|
||||
"canAccess": addr.CanAccess,
|
||||
"isUp": addr.IsUp,
|
||||
"isOn": addr.IsOn,
|
||||
"clusters": addrClusterMaps,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -175,7 +185,7 @@ func (this *NodesAction) RunGet(params struct {
|
||||
}
|
||||
|
||||
// DNS
|
||||
dnsRouteNames := []string{}
|
||||
var dnsRouteNames = []string{}
|
||||
for _, route := range node.DnsRoutes {
|
||||
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
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
|
||||
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/pb"
|
||||
"github.com/iwind/TeaGo/actions"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
stringutil "github.com/iwind/TeaGo/utils/string"
|
||||
"github.com/xlzd/gotp"
|
||||
"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 {
|
||||
From string
|
||||
@@ -59,7 +57,7 @@ func (this *IndexAction) RunGet(params struct {
|
||||
this.Data["menu"] = "signIn"
|
||||
|
||||
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
|
||||
|
||||
uiConfig, err := configloaders.LoadAdminUIConfig()
|
||||
@@ -93,9 +91,10 @@ func (this *IndexAction) RunPost(params struct {
|
||||
Password string
|
||||
OtpCode string
|
||||
Remember bool
|
||||
Must *actions.Must
|
||||
Auth *helpers.UserShouldAuth
|
||||
CSRF *actionutils.CSRF
|
||||
|
||||
Must *actions.Must
|
||||
Auth *helpers.UserShouldAuth
|
||||
CSRF *actionutils.CSRF
|
||||
}) {
|
||||
params.Must.
|
||||
Field("username", params.Username).
|
||||
@@ -112,7 +111,7 @@ func (this *IndexAction) RunPost(params struct {
|
||||
this.Fail("请通过登录页面登录")
|
||||
}
|
||||
var timestampString = params.Token[32:]
|
||||
if stringutil.Md5(TokenSalt+timestampString) != params.Token[:32] {
|
||||
if stringutil.Md5(TokenKey+timestampString) != params.Token[:32] {
|
||||
this.FailField("refresh", "登录页面已过期,请刷新后重试")
|
||||
}
|
||||
var timestamp = types.Int64(timestampString)
|
||||
@@ -123,6 +122,7 @@ func (this *IndexAction) RunPost(params struct {
|
||||
rpcClient, err := rpc.SharedRPC()
|
||||
if err != nil {
|
||||
this.Fail("服务器出了点小问题:" + err.Error())
|
||||
return
|
||||
}
|
||||
resp, err := rpcClient.AdminRPC().LoginAdmin(rpcClient.Context(0), &pb.LoginAdminRequest{
|
||||
Username: params.Username,
|
||||
@@ -136,6 +136,7 @@ func (this *IndexAction) RunPost(params struct {
|
||||
}
|
||||
|
||||
actionutils.Fail(this, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !resp.IsOk {
|
||||
@@ -145,31 +146,37 @@ func (this *IndexAction) RunPost(params struct {
|
||||
}
|
||||
|
||||
this.Fail("请输入正确的用户名密码")
|
||||
return
|
||||
}
|
||||
var adminId = resp.AdminId
|
||||
|
||||
// 检查OTP
|
||||
otpLoginResp, err := this.RPC().LoginRPC().FindEnabledLogin(this.AdminContext(), &pb.FindEnabledLoginRequest{
|
||||
AdminId: resp.AdminId,
|
||||
Type: "otp",
|
||||
})
|
||||
// 检查是否支持OTP
|
||||
checkOTPResp, err := this.RPC().AdminRPC().CheckAdminOTPWithUsername(this.AdminContext(), &pb.CheckAdminOTPWithUsernameRequest{Username: params.Username})
|
||||
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)
|
||||
var requireOTP = checkOTPResp.RequireOTP
|
||||
this.Data["requireOTP"] = requireOTP
|
||||
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 {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
secret := loginParams.GetString("secret")
|
||||
if gotp.NewDefaultTOTP(secret).Now() != params.OtpCode {
|
||||
this.Fail("请输入正确的OTP动态密码")
|
||||
}
|
||||
this.Success()
|
||||
return
|
||||
}
|
||||
|
||||
var adminId = resp.AdminId
|
||||
// 写入SESSION
|
||||
params.Auth.StoreAdmin(adminId, params.Remember)
|
||||
|
||||
// 记录日志
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
func init() {
|
||||
TeaGo.BeforeStart(func(server *TeaGo.Server) {
|
||||
server.
|
||||
Post("/checkOTP", new(CheckOTPAction)).
|
||||
Prefix("/").
|
||||
GetPost("", new(IndexAction)).
|
||||
Prefix("").
|
||||
GetPost("/", new(IndexAction)).
|
||||
GetPost("/index/otp", new(OtpAction)).
|
||||
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"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
|
||||
"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/iwind/TeaGo/actions"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
@@ -20,8 +21,18 @@ func (this *CreatePopupAction) Init() {
|
||||
}
|
||||
|
||||
func (this *CreatePopupAction) RunGet(params struct {
|
||||
NodeId int64
|
||||
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.Show()
|
||||
@@ -33,6 +44,7 @@ func (this *CreatePopupAction) RunPost(params struct {
|
||||
Name string
|
||||
IsUp bool
|
||||
ThresholdsJSON []byte
|
||||
ClusterIds []int64
|
||||
|
||||
Must *actions.Must
|
||||
}) {
|
||||
@@ -57,6 +69,14 @@ func (this *CreatePopupAction) RunPost(params struct {
|
||||
_ = 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{
|
||||
"name": params.Name,
|
||||
"canAccess": params.CanAccess,
|
||||
@@ -65,6 +85,7 @@ func (this *CreatePopupAction) RunPost(params struct {
|
||||
"isOn": true,
|
||||
"isUp": params.IsUp,
|
||||
"thresholds": thresholds,
|
||||
"clusters": clusterMaps,
|
||||
}
|
||||
this.Success()
|
||||
}
|
||||
|
||||
@@ -11,14 +11,28 @@ import (
|
||||
|
||||
// UpdateNodeIPAddresses 保存一组IP地址
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, addr := range addresses {
|
||||
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 {
|
||||
resultAddrIds = append(resultAddrIds, addrId)
|
||||
|
||||
@@ -36,6 +50,7 @@ func UpdateNodeIPAddresses(parentAction *actionutils.ParentAction, nodeId int64,
|
||||
CanAccess: addr.GetBool("canAccess"),
|
||||
IsOn: isOn,
|
||||
IsUp: addr.GetBool("isUp"),
|
||||
ClusterIds: addrClusterIds,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -47,12 +62,13 @@ func UpdateNodeIPAddresses(parentAction *actionutils.ParentAction, nodeId int64,
|
||||
if len(result) == 1 {
|
||||
// 单个创建
|
||||
createResp, err := parentAction.RPC().NodeIPAddressRPC().CreateNodeIPAddress(parentAction.AdminContext(), &pb.CreateNodeIPAddressRequest{
|
||||
NodeId: nodeId,
|
||||
Role: role,
|
||||
Name: addr.GetString("name"),
|
||||
Ip: result[0],
|
||||
CanAccess: addr.GetBool("canAccess"),
|
||||
IsUp: addr.GetBool("isUp"),
|
||||
NodeId: nodeId,
|
||||
Role: role,
|
||||
Name: addr.GetString("name"),
|
||||
Ip: result[0],
|
||||
CanAccess: addr.GetBool("canAccess"),
|
||||
IsUp: addr.GetBool("isUp"),
|
||||
NodeClusterIds: addrClusterIds,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -62,13 +78,14 @@ func UpdateNodeIPAddresses(parentAction *actionutils.ParentAction, nodeId int64,
|
||||
} else if len(result) > 1 {
|
||||
// 批量创建
|
||||
createResp, err := parentAction.RPC().NodeIPAddressRPC().CreateNodeIPAddresses(parentAction.AdminContext(), &pb.CreateNodeIPAddressesRequest{
|
||||
NodeId: nodeId,
|
||||
Role: role,
|
||||
Name: addr.GetString("name"),
|
||||
IpList: result,
|
||||
CanAccess: addr.GetBool("canAccess"),
|
||||
IsUp: addr.GetBool("isUp"),
|
||||
GroupValue: ipStrings,
|
||||
NodeId: nodeId,
|
||||
Role: role,
|
||||
Name: addr.GetString("name"),
|
||||
IpList: result,
|
||||
CanAccess: addr.GetBool("canAccess"),
|
||||
IsUp: addr.GetBool("isUp"),
|
||||
GroupValue: ipStrings,
|
||||
NodeClusterIds: addrClusterIds,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -140,3 +157,53 @@ func InitNodeIPAddressThresholds(parentAction *actionutils.ParentAction, addrId
|
||||
}
|
||||
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"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
|
||||
"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/rpc/pb"
|
||||
"github.com/iwind/TeaGo/actions"
|
||||
@@ -21,9 +22,18 @@ func (this *UpdatePopupAction) Init() {
|
||||
}
|
||||
|
||||
func (this *UpdatePopupAction) RunGet(params struct {
|
||||
NodeId int64
|
||||
AddressId int64
|
||||
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.Show()
|
||||
@@ -37,6 +47,7 @@ func (this *UpdatePopupAction) RunPost(params struct {
|
||||
IsOn bool
|
||||
IsUp bool
|
||||
ThresholdsJSON []byte
|
||||
ClusterIds []int64
|
||||
|
||||
Must *actions.Must
|
||||
}) {
|
||||
@@ -81,6 +92,14 @@ func (this *UpdatePopupAction) RunPost(params struct {
|
||||
_ = 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{
|
||||
"name": params.Name,
|
||||
"ip": params.IP,
|
||||
@@ -89,6 +108,7 @@ func (this *UpdatePopupAction) RunPost(params struct {
|
||||
"isOn": params.IsOn,
|
||||
"isUp": isUp,
|
||||
"thresholds": thresholds,
|
||||
"clusters": clusterMaps,
|
||||
}
|
||||
|
||||
this.Success()
|
||||
|
||||
@@ -30,6 +30,9 @@ func (this *IndexAction) RunGet(params struct {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
|
||||
this.Data["regions"] = ""
|
||||
this.Data["isp"] = ""
|
||||
if regionResp.IpRegion != nil {
|
||||
var regionName = regionResp.IpRegion.Summary
|
||||
|
||||
@@ -39,10 +42,8 @@ func (this *IndexAction) RunGet(params struct {
|
||||
regionName = regionName[:index]
|
||||
}
|
||||
this.Data["regions"] = regionName
|
||||
} else {
|
||||
this.Data["regions"] = ""
|
||||
this.Data["isp"] = regionResp.IpRegion.Isp
|
||||
}
|
||||
this.Data["isp"] = regionResp.IpRegion.Isp
|
||||
|
||||
// IP列表
|
||||
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 {
|
||||
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,
|
||||
})
|
||||
|
||||
if teaconst.IsPlus {
|
||||
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 = filterMenuItems(serverConfig, menuItems, serverIdString, secondMenuItem)
|
||||
|
||||
menuItems = append(menuItems, maps.Map{
|
||||
"name": "-",
|
||||
@@ -406,7 +392,7 @@ func (this *ServerHelper) createSettingsMenu(secondMenuItem string, serverIdStri
|
||||
"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{
|
||||
"name": "-",
|
||||
|
||||
@@ -58,6 +58,7 @@ func (this *IndexAction) RunPost(params struct {
|
||||
var apiURL = teaconst.UpdatesURL
|
||||
apiURL = strings.ReplaceAll(apiURL, "${os}", runtime.GOOS)
|
||||
apiURL = strings.ReplaceAll(apiURL, "${arch}", runtime.GOARCH)
|
||||
apiURL = strings.ReplaceAll(apiURL, "${version}", teaconst.Version)
|
||||
resp, err := http.Get(apiURL)
|
||||
if err != nil {
|
||||
this.Data["result"] = maps.Map{
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -63,10 +64,11 @@ func (this *DetectDBAction) RunPost(params struct{}) {
|
||||
}
|
||||
|
||||
this.Data["localDB"] = maps.Map{
|
||||
"host": localHost,
|
||||
"port": localPort,
|
||||
"username": localUsername,
|
||||
"password": localPassword,
|
||||
"host": localHost,
|
||||
"port": localPort,
|
||||
"username": localUsername,
|
||||
"password": localPassword,
|
||||
"canInstall": runtime.GOOS == "linux" && runtime.GOARCH == "amd64" && os.Getgid() == 0,
|
||||
}
|
||||
|
||||
this.Success()
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package setup
|
||||
|
||||
import "github.com/iwind/TeaGo"
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/setup/mysql"
|
||||
"github.com/iwind/TeaGo"
|
||||
)
|
||||
|
||||
func init() {
|
||||
TeaGo.BeforeStart(func(server *TeaGo.Server) {
|
||||
@@ -15,6 +18,8 @@ func init() {
|
||||
Post("/status", new(StatusAction)).
|
||||
Post("/detectDB", new(DetectDBAction)).
|
||||
Post("/checkLocalIP", new(CheckLocalIPAction)).
|
||||
GetPost("/mysql/installPopup", new(mysql.InstallPopupAction)).
|
||||
Post("/mysql/installLogs", new(mysql.InstallLogsAction)).
|
||||
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/types"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -61,7 +62,24 @@ func (this *ValidateApiAction) RunPost(params struct {
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
@@ -68,6 +68,7 @@ func (this *UpdateAction) RunGet(params struct {
|
||||
"mobile": user.Mobile,
|
||||
"isOn": user.IsOn,
|
||||
"countAccessKeys": countAccessKeys,
|
||||
"bandwidthAlgo": user.BandwidthAlgo,
|
||||
|
||||
// 实名认证
|
||||
"hasNewIndividualIdentity": hasNewIndividualIdentity,
|
||||
@@ -87,17 +88,18 @@ func (this *UpdateAction) RunGet(params struct {
|
||||
}
|
||||
|
||||
func (this *UpdateAction) RunPost(params struct {
|
||||
UserId int64
|
||||
Username string
|
||||
Pass1 string
|
||||
Pass2 string
|
||||
Fullname string
|
||||
Mobile string
|
||||
Tel string
|
||||
Email string
|
||||
Remark string
|
||||
IsOn bool
|
||||
ClusterId int64
|
||||
UserId int64
|
||||
Username string
|
||||
Pass1 string
|
||||
Pass2 string
|
||||
Fullname string
|
||||
Mobile string
|
||||
Tel string
|
||||
Email string
|
||||
Remark string
|
||||
IsOn bool
|
||||
ClusterId int64
|
||||
BandwidthAlgo string
|
||||
|
||||
// OTP
|
||||
OtpOn bool
|
||||
@@ -159,6 +161,7 @@ func (this *UpdateAction) RunPost(params struct {
|
||||
Remark: params.Remark,
|
||||
IsOn: params.IsOn,
|
||||
NodeClusterId: params.ClusterId,
|
||||
BandwidthAlgo: params.BandwidthAlgo,
|
||||
})
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
|
||||
@@ -109,6 +109,7 @@ func (this *UserAction) RunGet(params struct {
|
||||
"isVerified": user.IsVerified,
|
||||
"registeredIP": user.RegisteredIP,
|
||||
"registeredRegion": registeredRegion,
|
||||
"bandwidthAlgo": user.BandwidthAlgo,
|
||||
|
||||
// 实名认证
|
||||
"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", {
|
||||
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 () {
|
||||
let values = this.values;
|
||||
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) {
|
||||
Vue.set(this.realValues, this.index, this.value);
|
||||
} else {
|
||||
@@ -78,16 +94,16 @@ Vue.component("values-box", {
|
||||
template: `<div>
|
||||
<div v-show="!isEditing && realValues.length > 0">
|
||||
<div class="ui label tiny basic" v-for="(value, index) in realValues" style="margin-top:0.4em;margin-bottom:0.4em">
|
||||
<span v-if="value.length > 0">{{value}}</span>
|
||||
<span v-if="value.length == 0" class="disabled">[空]</span>
|
||||
<span v-if="value.toString().length > 0">{{value}}</span>
|
||||
<span v-if="value.toString().length == 0" class="disabled">[空]</span>
|
||||
</div>
|
||||
<a href="" @click.prevent="startEditing" style="font-size: 0.8em; margin-left: 0.2em">[修改]</a>
|
||||
</div>
|
||||
<div v-show="isEditing || realValues.length == 0">
|
||||
<div style="margin-bottom: 1em" v-if="realValues.length > 0">
|
||||
<div class="ui label tiny basic" v-for="(value, index) in realValues" style="margin-top:0.4em;margin-bottom:0.4em">
|
||||
<span v-if="value.length > 0">{{value}}</span>
|
||||
<span v-if="value.length == 0" class="disabled">[空]</span>
|
||||
<span v-if="value.toString().length > 0">{{value}}</span>
|
||||
<span v-if="value.toString().length == 0" class="disabled">[空]</span>
|
||||
<input type="hidden" :name="name" :value="value"/>
|
||||
<a href="" @click.prevent="update(index)" title="修改"><i class="icon pencil small" ></i></a>
|
||||
<a href="" @click.prevent="remove(index)" title="删除"><i class="icon remove"></i></a>
|
||||
|
||||
@@ -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地址管理(标签形式)
|
||||
Vue.component("node-ip-addresses-box", {
|
||||
props: ["v-ip-addresses", "role"],
|
||||
props: ["v-ip-addresses", "role", "v-node-id"],
|
||||
data: function () {
|
||||
let nodeId = this.vNodeId
|
||||
if (nodeId == null) {
|
||||
nodeId = 0
|
||||
}
|
||||
|
||||
return {
|
||||
ipAddresses: (this.vIpAddresses == null) ? [] : this.vIpAddresses,
|
||||
supportThresholds: this.role != "ns"
|
||||
supportThresholds: this.role != "ns",
|
||||
nodeId: nodeId
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -13,7 +19,7 @@ Vue.component("node-ip-addresses-box", {
|
||||
window.UPDATING_NODE_IP_ADDRESS = null
|
||||
|
||||
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) {
|
||||
that.ipAddresses.push(resp.data.ipAddress);
|
||||
},
|
||||
@@ -24,10 +30,10 @@ Vue.component("node-ip-addresses-box", {
|
||||
|
||||
// 修改地址
|
||||
updateIPAddress: function (index, address) {
|
||||
window.UPDATING_NODE_IP_ADDRESS = address
|
||||
window.UPDATING_NODE_IP_ADDRESS = teaweb.clone(address)
|
||||
|
||||
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) {
|
||||
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" 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="removeIPAddress(index)"><i class="icon remove"></i></a>
|
||||
</div>
|
||||
|
||||
@@ -48,7 +48,7 @@ Vue.component("firewall-syn-flood-config-box", {
|
||||
<input type="hidden" name="synFloodJSON" :value="JSON.stringify(config)"/>
|
||||
<a href="" @click.prevent="edit">
|
||||
<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 v-else>未启用</span>
|
||||
<i class="icon angle" :class="{up: isEditing, down: !isEditing}"></i>
|
||||
@@ -73,7 +73,7 @@ Vue.component("firewall-syn-flood-config-box", {
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>封禁时间</td>
|
||||
<td>封禁时长</td>
|
||||
<td>
|
||||
<div class="ui input right labeled">
|
||||
<input type="text" v-model="timeoutSeconds" style="width: 5em" maxlength="8"/>
|
||||
|
||||
@@ -16,7 +16,7 @@ Vue.component("firewall-syn-flood-config-viewer", {
|
||||
},
|
||||
template: `<div>
|
||||
<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 v-else>未启用</span>
|
||||
</div>`
|
||||
|
||||
@@ -56,18 +56,22 @@ Vue.component("http-cache-config-box", {
|
||||
},
|
||||
template: `<div>
|
||||
<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">
|
||||
<prior-checkbox :v-config="cacheConfig" v-if="vIsLocation || vIsGroup"></prior-checkbox>
|
||||
<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>
|
||||
<td class="title">启用缓存</td>
|
||||
<td>
|
||||
@@ -90,7 +94,7 @@ Vue.component("http-cache-config-box", {
|
||||
<td>使用默认缓存条件</td>
|
||||
<td>
|
||||
<checkbox v-model="enablePolicyRefs"></checkbox>
|
||||
<p class="comment">选中后使用系统中已经定义的默认缓存条件。</p>
|
||||
<p class="comment">选中后使用系统全局缓存策略中已经定义的默认缓存条件。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -190,7 +190,10 @@ Vue.component("http-cache-ref-box", {
|
||||
<tr v-if="condCategory == 'simple'">
|
||||
<td class="color-border">{{condComponent.paramsTitle}} *</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>
|
||||
</tr>
|
||||
<tr v-if="condCategory == 'simple' && condComponent.caseInsensitive">
|
||||
|
||||
@@ -75,7 +75,7 @@ Vue.component("http-cache-refs-config-box", {
|
||||
})
|
||||
},
|
||||
updateRef: function (index, cacheRef) {
|
||||
window.UPDATING_CACHE_REF = cacheRef
|
||||
window.UPDATING_CACHE_REF = teaweb.clone(cacheRef)
|
||||
|
||||
let height = window.innerHeight
|
||||
if (height > 500) {
|
||||
|
||||
@@ -4,7 +4,7 @@ Vue.component("http-cond-url-extension", {
|
||||
data: function () {
|
||||
let cond = {
|
||||
isRequest: true,
|
||||
param: "${requestPathExtension}",
|
||||
param: "${requestPathLowerExtension}",
|
||||
operator: "in",
|
||||
value: "[]"
|
||||
}
|
||||
@@ -95,7 +95,7 @@ Vue.component("http-cond-url-not-extension", {
|
||||
data: function () {
|
||||
let cond = {
|
||||
isRequest: true,
|
||||
param: "${requestPathExtension}",
|
||||
param: "${requestPathLowerExtension}",
|
||||
operator: "not in",
|
||||
value: "[]"
|
||||
}
|
||||
@@ -718,21 +718,19 @@ Vue.component("http-cond-params", {
|
||||
},
|
||||
template: `<tbody>
|
||||
<tr>
|
||||
<td>参数值</td>
|
||||
<td style="width: 8em">参数值</td>
|
||||
<td>
|
||||
<input type="hidden" name="condJSON" :value="JSON.stringify(cond)"/>
|
||||
<div>
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field">
|
||||
<input type="text" placeholder="\${xxx}" v-model="cond.param"/>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<select class="ui dropdown" style="width: 7em; color: grey" v-model="variable" @change="changeVariable">
|
||||
<option value="">[常用参数]</option>
|
||||
<option v-for="v in variables" :value="v.code">{{v.code}} - {{v.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<input type="text" placeholder="\${xxx}" v-model="cond.param"/>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<select class="ui dropdown" style="width: 16em; color: grey" v-model="variable" @change="changeVariable">
|
||||
<option value="">[常用参数]</option>
|
||||
<option v-for="v in variables" :value="v.code">{{v.code}} - {{v.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p class="comment">其中可以使用变量,类似于<code-label>\${requestPath}</code-label>,也可以是多个变量的组合。</p>
|
||||
</td>
|
||||
@@ -835,5 +833,6 @@ Vue.component("http-cond-params", {
|
||||
<p class="comment">选中后表示对比时忽略参数值的大小写。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>`
|
||||
</tbody>
|
||||
`
|
||||
})
|
||||
@@ -73,6 +73,7 @@ Vue.component("http-firewall-actions-box", {
|
||||
|
||||
// 动作参数
|
||||
blockTimeout: "",
|
||||
blockTimeoutMax: "",
|
||||
blockScope: "global",
|
||||
|
||||
captchaLife: "",
|
||||
@@ -122,6 +123,14 @@ Vue.component("http-firewall-actions-box", {
|
||||
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) {
|
||||
this.actionOptions["scope"] = v
|
||||
},
|
||||
@@ -237,6 +246,7 @@ Vue.component("http-firewall-actions-box", {
|
||||
|
||||
// 动作参数
|
||||
this.blockTimeout = ""
|
||||
this.blockTimeoutMax = ""
|
||||
this.blockScope = "global"
|
||||
|
||||
this.captchaLife = ""
|
||||
@@ -298,9 +308,13 @@ Vue.component("http-firewall-actions-box", {
|
||||
switch (config.code) {
|
||||
case "block":
|
||||
this.blockTimeout = ""
|
||||
this.blockTimeoutMax = ""
|
||||
if (config.options.timeout != null || config.options.timeout > 0) {
|
||||
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) {
|
||||
this.blockScope = config.options.scope
|
||||
} else {
|
||||
@@ -584,7 +598,7 @@ Vue.component("http-firewall-actions-box", {
|
||||
{{config.name}} <span class="small">({{config.code.toUpperCase()}})</span>
|
||||
|
||||
<!-- 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 -->
|
||||
<span v-if="config.code == 'captcha' && config.options.life > 0">:有效期{{config.options.life}}秒
|
||||
@@ -643,7 +657,18 @@ Vue.component("http-firewall-actions-box", {
|
||||
|
||||
<!-- 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>
|
||||
<div class="ui input right labeled">
|
||||
<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>
|
||||
</tr>
|
||||
<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>
|
||||
<div class="ui input right labeled">
|
||||
<input type="text" style="width: 5em" maxlength="9" v-model="blockTimeoutMax" @keyup.enter="confirm()" @keypress.enter.prevent="1"/>
|
||||
<span class="ui label">秒</span>
|
||||
</div>
|
||||
<p class="comment">选填项。如果同时填写了封禁时长和最大封禁时长,则会在两者之间随机选择一个数字作为最终的封禁时长。</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ Vue.component("http-firewall-block-options-viewer", {
|
||||
template: `<div>
|
||||
<span v-if="options == null">默认设置</span>
|
||||
<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>
|
||||
`
|
||||
|
||||
@@ -5,6 +5,7 @@ Vue.component("http-firewall-block-options", {
|
||||
blockOptions: this.vBlockOptions,
|
||||
statusCode: this.vBlockOptions.statusCode,
|
||||
timeout: this.vBlockOptions.timeout,
|
||||
timeoutMax: this.vBlockOptions.timeoutMax,
|
||||
isEditing: false
|
||||
}
|
||||
},
|
||||
@@ -24,6 +25,14 @@ Vue.component("http-firewall-block-options", {
|
||||
} else {
|
||||
this.blockOptions.timeout = timeout
|
||||
}
|
||||
},
|
||||
timeoutMax: function (v) {
|
||||
let timeoutMax = parseInt(v)
|
||||
if (isNaN(timeoutMax)) {
|
||||
this.blockOptions.timeoutMax = 0
|
||||
} else {
|
||||
this.blockOptions.timeoutMax = timeoutMax
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -33,7 +42,9 @@ Vue.component("http-firewall-block-options", {
|
||||
},
|
||||
template: `<div>
|
||||
<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">
|
||||
<tr>
|
||||
<td class="title">状态码</td>
|
||||
@@ -48,13 +59,23 @@ Vue.component("http-firewall-block-options", {
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>超时时间</td>
|
||||
<td>封禁时长</td>
|
||||
<td>
|
||||
<div class="ui input right labeled">
|
||||
<input type="text" v-model="timeout" style="width: 5em" maxlength="6"/>
|
||||
<span class="ui label">秒</span>
|
||||
</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>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -6,36 +6,60 @@ Vue.component("http-firewall-config-box", {
|
||||
firewall = {
|
||||
isPrior: false,
|
||||
isOn: false,
|
||||
firewallPolicyId: 0
|
||||
firewallPolicyId: 0,
|
||||
ignoreGlobalRules: false
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
<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">
|
||||
<prior-checkbox :v-config="firewall" v-if="vIsLocation || vIsGroup"></prior-checkbox>
|
||||
<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>
|
||||
<td class="title">启用WAF</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" v-model="firewall.isOn"/>
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">启用WAF之后,各项WAF设置才会生效。</p>
|
||||
<checkbox v-model="firewall.isOn"></checkbox>
|
||||
<p class="comment">选中后,表示启用当前网站服务的WAF功能。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</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>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -21,6 +21,9 @@ Vue.component("http-firewall-rule-label", {
|
||||
}
|
||||
|
||||
return operatorName
|
||||
},
|
||||
isEmptyString: function (v) {
|
||||
return typeof v == "string" && v.length == 0
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
@@ -40,8 +43,9 @@ Vue.component("http-firewall-rule-label", {
|
||||
|
||||
<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 ? '大小写不敏感':''" v-if="!rule.isComposed">{{operatorName(rule.operator)}}</span>
|
||||
{{rule.value}}
|
||||
<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>
|
||||
|
||||
<!-- description -->
|
||||
|
||||
@@ -47,6 +47,9 @@ Vue.component("http-firewall-rules-box", {
|
||||
}
|
||||
|
||||
return operatorName
|
||||
},
|
||||
isEmptyString: function (v) {
|
||||
return typeof v == "string" && v.length == 0
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
@@ -67,7 +70,9 @@ Vue.component("http-firewall-rules-box", {
|
||||
</span>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- description -->
|
||||
|
||||
@@ -93,6 +93,7 @@ Vue.component("http-firewall-checkpoint-cc", {
|
||||
let period = 60
|
||||
let threshold = 1000
|
||||
let ignoreCommonFiles = false
|
||||
let enableFingerprint = true
|
||||
|
||||
let options = {}
|
||||
if (window.parent.UPDATING_RULE != null) {
|
||||
@@ -117,6 +118,9 @@ Vue.component("http-firewall-checkpoint-cc", {
|
||||
if (options.ignoreCommonFiles != null && typeof (options.ignoreCommonFiles) == "boolean") {
|
||||
ignoreCommonFiles = options.ignoreCommonFiles
|
||||
}
|
||||
if (options.enableFingerprint != null && typeof (options.enableFingerprint) == "boolean") {
|
||||
enableFingerprint = options.enableFingerprint
|
||||
}
|
||||
|
||||
let that = this
|
||||
setTimeout(function () {
|
||||
@@ -128,6 +132,7 @@ Vue.component("http-firewall-checkpoint-cc", {
|
||||
period: period,
|
||||
threshold: threshold,
|
||||
ignoreCommonFiles: ignoreCommonFiles,
|
||||
enableFingerprint: enableFingerprint,
|
||||
options: {},
|
||||
value: threshold
|
||||
}
|
||||
@@ -141,6 +146,9 @@ Vue.component("http-firewall-checkpoint-cc", {
|
||||
},
|
||||
ignoreCommonFiles: function () {
|
||||
this.change()
|
||||
},
|
||||
enableFingerprint: function () {
|
||||
this.change()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -165,6 +173,11 @@ Vue.component("http-firewall-checkpoint-cc", {
|
||||
ignoreCommonFiles = false
|
||||
}
|
||||
|
||||
let enableFingerprint = this.enableFingerprint
|
||||
if (typeof enableFingerprint != "boolean") {
|
||||
enableFingerprint = true
|
||||
}
|
||||
|
||||
this.vCheckpoint.options = [
|
||||
{
|
||||
code: "keys",
|
||||
@@ -181,6 +194,10 @@ Vue.component("http-firewall-checkpoint-cc", {
|
||||
{
|
||||
code: "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>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>检查请求来源指纹</td>
|
||||
<td>
|
||||
<checkbox v-model="enableFingerprint"></checkbox>
|
||||
<p class="comment">在接收到HTTPS请求时尝试检查请求来源的指纹,用来检测代理服务和爬虫攻击。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
|
||||
<!-- 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 -->
|
||||
<!-- TODO -->
|
||||
|
||||
|
||||
@@ -27,8 +27,16 @@ Vue.component("http-referers-config-box", {
|
||||
return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn
|
||||
},
|
||||
changeAllowDomains: function (domains) {
|
||||
if (typeof (domains) == "object") {
|
||||
this.config.allowDomains = domains
|
||||
this.$forceUpdate()
|
||||
}
|
||||
},
|
||||
changeDenyDomains: function (domains) {
|
||||
if (typeof (domains) == "object") {
|
||||
this.config.denyDomains = domains
|
||||
this.$forceUpdate()
|
||||
}
|
||||
}
|
||||
},
|
||||
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) {
|
||||
Swal.fire({
|
||||
html: '<div style="line-height: 1.7;text-align: left "><i class="icon question circle"></i>' + html + "</div>",
|
||||
width: "30em",
|
||||
width: "34em",
|
||||
padding: "4em",
|
||||
showConfirmButton: false,
|
||||
showCloseButton: true,
|
||||
@@ -991,7 +991,45 @@ window.teaweb = {
|
||||
}
|
||||
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 () {
|
||||
|
||||
@@ -7,5 +7,4 @@
|
||||
<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 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>
|
||||
@@ -23,7 +23,12 @@
|
||||
</thead>
|
||||
<tr v-for="node in nodes">
|
||||
<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>
|
||||
<div v-if="node.accessAddrs != null && node.accessAddrs.length > 0">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Tea.context(function () {
|
||||
// 创建节点
|
||||
this.createNode = function () {
|
||||
teaweb.popup("/api/node/createPopup", {
|
||||
teaweb.popup(".node.createPopup", {
|
||||
width: "50em",
|
||||
height: "30em",
|
||||
callback: function () {
|
||||
@@ -16,11 +16,20 @@ Tea.context(function () {
|
||||
this.deleteNode = function (nodeId) {
|
||||
let that = this
|
||||
teaweb.confirm("确定要删除此节点吗?", function () {
|
||||
that.$post("/api/delete")
|
||||
that.$post(".delete")
|
||||
.params({
|
||||
nodeId: nodeId
|
||||
})
|
||||
.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>
|
||||
<div v-if="node.ipAddresses.length > 0">
|
||||
<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 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>
|
||||
@@ -30,6 +30,10 @@
|
||||
<span class="small red" v-if="!address.isOn">[off]</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 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>
|
||||
@@ -51,6 +55,7 @@
|
||||
<table class="ui table celled">
|
||||
<thead class="full-width">
|
||||
<tr>
|
||||
<th>集群</th>
|
||||
<th>记录名</th>
|
||||
<th>记录类型</th>
|
||||
<th>线路</th>
|
||||
@@ -59,6 +64,7 @@
|
||||
</thead>
|
||||
|
||||
<tr v-for="record in node.records">
|
||||
<td>{{record.clusterName}}</td>
|
||||
<td>{{record.name}}</td>
|
||||
<td>{{record.type}}</td>
|
||||
<td>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<tr>
|
||||
<td>IP地址 *</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>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -94,6 +94,11 @@
|
||||
<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.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>
|
||||
|
||||
@@ -77,6 +77,13 @@
|
||||
<p class="comment">选中后,表示访问日志中记录Cookie内容。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>记录找不到网站日志</td>
|
||||
<td>
|
||||
<checkbox name="httpAccessLogEnableServerNotFound" v-model="config.httpAccessLog.enableServerNotFound"></checkbox>
|
||||
<p class="comment">选中后,表示如果访客访问的域名对应的网站不存在也会记录日志。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h4>运行日志</h4>
|
||||
|
||||
@@ -2,4 +2,11 @@ pre.log-box {
|
||||
margin: 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 */
|
||||
@@ -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>
|
||||
|
||||
<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">
|
||||
<thead>
|
||||
<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 {
|
||||
margin: 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}}
|
||||
<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>
|
||||
|
||||
<!-- 专属集群 -->
|
||||
<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>
|
||||
|
||||
@@ -41,12 +41,6 @@
|
||||
<input type="password" v-model="password" placeholder="请输入密码" maxlength="200" @input="changePassword()" ref="passwordRef"/>
|
||||
</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">
|
||||
<a href="" @click.prevent="showMoreOptions()">更多选项 <i class="icon angle" :class="{down:!moreOptionsVisible, up:moreOptionsVisible}"></i> </a>
|
||||
</div>
|
||||
|
||||
@@ -9,8 +9,6 @@ Tea.context(function () {
|
||||
this.password = "123456"
|
||||
}
|
||||
|
||||
this.showOTP = false
|
||||
|
||||
this.isSubmitting = false
|
||||
|
||||
this.$delay(function () {
|
||||
@@ -19,13 +17,7 @@ Tea.context(function () {
|
||||
});
|
||||
|
||||
this.changeUsername = function () {
|
||||
this.$post("/checkOTP")
|
||||
.params({
|
||||
username: this.username
|
||||
})
|
||||
.success(function (resp) {
|
||||
this.showOTP = resp.data.requireOTP
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
this.changePassword = function () {
|
||||
@@ -46,7 +38,11 @@ Tea.context(function () {
|
||||
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) {
|
||||
window.location = "/dashboard";
|
||||
} 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