初步实现安装界面

This commit is contained in:
GoEdgeLab
2020-10-13 20:05:29 +08:00
parent fe432e9a8f
commit 2c9fffa4db
37 changed files with 1207 additions and 56 deletions

1
build/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
edge-api/

View File

@@ -1,4 +1,4 @@
rpc:
endpoints: [ "http://127.0.0.1:8003" ]
nodeId: ""
secret: ""
nodeId: ""
secret: ""

View File

@@ -7,8 +7,11 @@ import (
"github.com/iwind/TeaGo"
"github.com/iwind/TeaGo/Tea"
_ "github.com/iwind/TeaGo/bootstrap"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/rands"
"github.com/iwind/TeaGo/sessions"
"os"
"os/exec"
)
func main() {
@@ -26,6 +29,16 @@ func main() {
secret = "8f983f4d69b83aaa0d74b21a212f6967"
}
// 启动API节点
_, err := os.Stat(Tea.Root + "/edge-api/configs/api.yaml")
if err == nil {
logs.Println("start edge-api")
err = exec.Command(Tea.Root + "/edge-api/bin/edge-api").Start()
if err != nil {
logs.Println("[ERROR]start edge-api failed: " + err.Error())
}
}
server := TeaGo.NewServer(false).
AccessLog(false).
EndAll().

11
go.mod
View File

@@ -6,15 +6,10 @@ replace github.com/TeaOSLab/EdgeCommon => ../EdgeCommon
require (
github.com/TeaOSLab/EdgeCommon v0.0.0-00010101000000-000000000000
github.com/go-redis/redis v6.15.8+incompatible // indirect
github.com/go-sql-driver/mysql v1.5.0
github.com/go-yaml/yaml v2.1.0+incompatible
github.com/golang/protobuf v1.4.2 // indirect
github.com/iwind/TeaGo v0.0.0-20200924024009-d088df3778a6
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7 // indirect
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa
github.com/iwind/TeaGo v0.0.0-20201010005321-430e836dee8a
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c // indirect
google.golang.org/grpc v1.32.0
google.golang.org/protobuf v1.25.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
)

26
go.sum
View File

@@ -3,7 +3,6 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/sketches-go v0.0.0-20190923095040-43f19ad77ff7/go.mod h1:Q5DbzQ+3AkgGwymQO7aZFNP7ns2lZKGtvRBzRXfdi60=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -20,8 +19,6 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/go-redis/redis v6.15.8+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-redis/redis/v8 v8.0.0-beta.7/go.mod h1:FGJAWDWFht1sQ4qxyJHZZbVyvnVcKQN0E3u5/5lRz+g=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
@@ -49,23 +46,12 @@ github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/iwind/TeaGo v0.0.0-20200723131229-30dff10543ad h1:EVwLRNPYoCNCinN/J9FylGBpKdCilvzUFykKtQABfNA=
github.com/iwind/TeaGo v0.0.0-20200723131229-30dff10543ad/go.mod h1:zjM7k+b+Jthhf0T0fKwuF0iy4TWb5SsU1gmKR2l+OmE=
github.com/iwind/TeaGo v0.0.0-20200816132655-f784df8e9c42 h1:X58QYxjoTstHR4sQEwx4ChAFRYtlWAfqwimQ3d2osT0=
github.com/iwind/TeaGo v0.0.0-20200816132655-f784df8e9c42/go.mod h1:KU4mS7QNiZ7QWEuDBk1zw0/Q2LrAPZv3tycEFBsuUwc=
github.com/iwind/TeaGo v0.0.0-20200822074248-b1cf7248c98a h1:VaWcMNOzHHT1y8MeTA2fWhG6GEfAdy6CwF2tW+KiY5Y=
github.com/iwind/TeaGo v0.0.0-20200822074248-b1cf7248c98a/go.mod h1:KU4mS7QNiZ7QWEuDBk1zw0/Q2LrAPZv3tycEFBsuUwc=
github.com/iwind/TeaGo v0.0.0-20200909062051-96811444bb22 h1:bCv4Emo49CZyZFbnq9lWNr8nYM1yrfhjuaM7KJEc0jk=
github.com/iwind/TeaGo v0.0.0-20200909062051-96811444bb22/go.mod h1:KU4mS7QNiZ7QWEuDBk1zw0/Q2LrAPZv3tycEFBsuUwc=
github.com/iwind/TeaGo v0.0.0-20200910072805-729cffe36729 h1:/v0WhSFVeNay/dA5zU9iCBXlgVDfxnztuanlauXE0gM=
github.com/iwind/TeaGo v0.0.0-20200910072805-729cffe36729/go.mod h1:KU4mS7QNiZ7QWEuDBk1zw0/Q2LrAPZv3tycEFBsuUwc=
github.com/iwind/TeaGo v0.0.0-20200923021120-f5d76441fe9e h1:/xn7wUvlwaoA5IkdBUctv2OQbJSZ0/Dw8qRJmn55sJk=
github.com/iwind/TeaGo v0.0.0-20200923021120-f5d76441fe9e/go.mod h1:KU4mS7QNiZ7QWEuDBk1zw0/Q2LrAPZv3tycEFBsuUwc=
github.com/iwind/TeaGo v0.0.0-20200924024009-d088df3778a6 h1:7OZC/Qy7Z/hK9vG6YQOwHNOUPunSImYYJMiIfvuDQZ0=
github.com/iwind/TeaGo v0.0.0-20200924024009-d088df3778a6/go.mod h1:KU4mS7QNiZ7QWEuDBk1zw0/Q2LrAPZv3tycEFBsuUwc=
github.com/iwind/TeaGo v0.0.0-20201010005321-430e836dee8a h1:sO6uDbQOEe6/tIB3o31vn6eD/JmkKGErKgcOA/Cpb+Q=
github.com/iwind/TeaGo v0.0.0-20201010005321-430e836dee8a/go.mod h1:KU4mS7QNiZ7QWEuDBk1zw0/Q2LrAPZv3tycEFBsuUwc=
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
@@ -89,14 +75,9 @@ github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/opentracing/opentracing-go v1.1.1-0.20190913142402-a7454ce5950e/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.12.0/go.mod h1:fUqqXB5vEgVCZ131L+9say31RAri6aF6KDViawhxKK8=
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/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7 h1:xoIK0ctDddBMnc74udxJYBqlo9Ylnsp1waqjLsnef20=
github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7/go.mod h1:YARuvh7BUWHNhzDq2OM5tzR2RiCcN2D7sapiKyCel/M=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/shirou/gopsutil v2.20.7+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
@@ -104,7 +85,6 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
go.opentelemetry.io/otel v0.7.0/go.mod h1:aZMyHG5TqDOXEgH2tyLiXSUKly1jT3yqE9PmrzIeCdo=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20200513190911-00229845015e/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
@@ -145,6 +125,8 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c h1:UIcGWL6/wpCfyGuJnRFJRurA+yj8RrW7Q6x2YMCXt6c=
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=

View File

@@ -6,6 +6,7 @@ import (
"io/ioutil"
)
// API配置
type APIConfig struct {
RPC struct {
Endpoints []string `yaml:"endpoints"`
@@ -14,6 +15,7 @@ type APIConfig struct {
Secret string `yaml:"secret"`
}
// 加载API配置
func LoadAPIConfig() (*APIConfig, error) {
data, err := ioutil.ReadFile(Tea.ConfigFile("api.yaml"))
if err != nil {
@@ -28,3 +30,12 @@ func LoadAPIConfig() (*APIConfig, error) {
return config, nil
}
// 写入API配置
func (this *APIConfig) WriteFile(path string) error {
data, err := yaml.Marshal(this)
if err != nil {
return err
}
return ioutil.WriteFile(path, data, 0666)
}

View File

@@ -175,7 +175,7 @@ func (this *RPCClient) SysSettingRPC() pb.SysSettingServiceClient {
return pb.NewSysSettingServiceClient(this.pickConn())
}
// 构造上下文
// 构造Admin上下文
func (this *RPCClient) Context(adminId int64) context.Context {
ctx := context.Background()
m := maps.Map{
@@ -198,6 +198,29 @@ func (this *RPCClient) Context(adminId int64) context.Context {
return ctx
}
// 构造API上下文
func (this *RPCClient) APIContext(apiNodeId int64) context.Context {
ctx := context.Background()
m := maps.Map{
"timestamp": time.Now().Unix(),
"type": "api",
"userId": apiNodeId,
}
method, err := encrypt.NewMethodInstance(teaconst.EncryptMethod, this.apiConfig.Secret, this.apiConfig.NodeId)
if err != nil {
utils.PrintError(err)
return context.Background()
}
data, err := method.Encrypt(m.AsJSON())
if err != nil {
utils.PrintError(err)
return context.Background()
}
token := base64.StdEncoding.EncodeToString(data)
ctx = metadata.AppendToOutgoingContext(ctx, "nodeId", this.apiConfig.NodeId, "token", token)
return ctx
}
// 随机选择一个连接
func (this *RPCClient) pickConn() *grpc.ClientConn {
if len(this.conns) == 0 {

18
internal/setup/utils.go Normal file
View File

@@ -0,0 +1,18 @@
package setup
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configs"
)
var isConfigured bool
// 判断系统是否已经配置过
func IsConfigured() bool {
if isConfigured {
return true
}
_, err := configs.LoadAPIConfig()
isConfigured = err == nil
return isConfigured
}

View File

@@ -4,10 +4,11 @@ import (
"fmt"
"github.com/TeaOSLab/EdgeAdmin/internal/oplogs"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"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/pb"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/types"
stringutil "github.com/iwind/TeaGo/utils/string"
@@ -25,6 +26,12 @@ func (this *IndexAction) RunGet(params struct {
Auth *helpers.UserShouldAuth
}) {
// 检查系统是否已经配置过
if !setup.IsConfigured() {
this.RedirectURL("/setup")
return
}
// 已登录跳转到dashboard
if params.Auth.IsUser() {
this.RedirectURL("/dashboard")

View File

@@ -41,16 +41,15 @@ func (this *IndexAction) RunGet(params struct{}) {
}
if config.DBs == nil {
this.Data["error"] = "can not find valid database config: api_db.yaml"
this.Data["error"] = "no database configured in config file: api_db.yaml"
this.Show()
return
}
dbConfig, ok := config.DBs[config.Default.DB]
if !ok {
this.Data["error"] = "can not find valid database config: api_db.yaml"
this.Show()
return
var dbConfig *dbs.DBConfig
for _, db := range config.DBs {
dbConfig = db
break
}
dsn := dbConfig.Dsn
dsn = regexp.MustCompile(`tcp\((.+)\)`).ReplaceAllString(dsn, "$1")

View File

@@ -50,11 +50,12 @@ func (this *UpdateAction) RunGet(params struct{}) {
return
}
dbConfig, ok := config.DBs[config.Default.DB]
if !ok {
this.Show()
return
var dbConfig *dbs.DBConfig
for _, db := range config.DBs {
dbConfig = db
break
}
dsn := dbConfig.Dsn
dsn = regexp.MustCompile(`tcp\((.+)\)`).ReplaceAllString(dsn, "$1")
dsnURL, err := url.Parse("mysql://" + dsn)

View File

@@ -0,0 +1,17 @@
package setup
import (
"github.com/TeaOSLab/EdgeAdmin/internal/setup"
"github.com/iwind/TeaGo/actions"
)
type Helper struct {
}
func (this *Helper) BeforeAction(actionPtr actions.ActionWrapper) (goNext bool) {
if setup.IsConfigured() {
actionPtr.Object().RedirectURL("/")
return false
}
return true
}

View File

@@ -0,0 +1,46 @@
package setup
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"net"
"regexp"
"sort"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "", "")
}
func (this *IndexAction) RunGet(params struct{}) {
// 当前服务器的IP
serverIPs := []string{}
addrs, _ := net.InterfaceAddrs()
for _, addr := range addrs {
netAddr, ok := addr.(*net.IPNet)
if !ok {
continue
}
serverIPs = append(serverIPs, netAddr.IP.String())
}
// 对IP进行排序我们希望IPv4排在前面而且希望127.0.0.1排在IPv4里的最后
sort.Slice(serverIPs, func(i, j int) bool {
ip1 := serverIPs[i]
if ip1 == "127.0.0.1" {
return false
}
if regexp.MustCompile(`^\d+\.\d+\.\d+.\d+$`).MatchString(ip1) {
return true
}
return false
})
this.Data["serverIPs"] = serverIPs
this.Show()
}

View File

@@ -0,0 +1,17 @@
package setup
import "github.com/iwind/TeaGo"
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(new(Helper)).
Prefix("/setup").
Get("", new(IndexAction)).
Post("/validateApi", new(ValidateApiAction)).
Post("/validateDb", new(ValidateDbAction)).
Post("/validateAdmin", new(ValidateAdminAction)).
Post("/install", new(InstallAction)).
EndAll()
})
}

View File

@@ -0,0 +1,203 @@
package setup
import (
"bytes"
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/configs"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/go-yaml/yaml"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/dbs"
"github.com/iwind/TeaGo/maps"
"io/ioutil"
"os"
"os/exec"
"time"
)
type InstallAction struct {
actionutils.ParentAction
}
func (this *InstallAction) RunPost(params struct {
ApiNodeJSON []byte
DbJSON []byte
AdminJSON []byte
Must *actions.Must
}) {
// API节点配置
apiNodeMap := maps.Map{}
err := json.Unmarshal(params.ApiNodeJSON, &apiNodeMap)
if err != nil {
this.Fail("API节点配置数据解析错误请刷新页面后重新尝试安装错误信息" + err.Error())
}
// 数据库
dbMap := maps.Map{}
err = json.Unmarshal(params.DbJSON, &dbMap)
if err != nil {
this.Fail("数据库配置数据解析错误,请刷新页面后重新尝试安装,错误信息:" + err.Error())
}
// 管理员
adminMap := maps.Map{}
err = json.Unmarshal(params.AdminJSON, &adminMap)
if err != nil {
this.Fail("管理员数据解析错误,请刷新页面后重新尝试安装,错误信息:" + err.Error())
}
// 安装API节点
mode := apiNodeMap.GetString("mode")
if mode == "new" {
// 整个系统目录结构为:
// edge-admin/
// edge-api/
// bin/
// ...
// 检查环境
apiNodeDir := Tea.Root + "/edge-api"
for _, dir := range []string{"edge-api", "edge-api/configs", "edge-api/bin"} {
apiNodeDir := Tea.Root + "/" + dir
_, err = os.Stat(apiNodeDir)
if err != nil {
if os.IsNotExist(err) {
this.Fail("在当前目录下找不到" + dir + "目录,请将" + dir + "目录上传或者重新下载解压")
}
this.Fail("无法检查" + dir + "目录,发生错误:" + err.Error())
}
}
// 保存数据库配置
dsn := dbMap.GetString("username") + ":" + dbMap.GetString("password") + "@tcp(" + dbMap.GetString("host") + ":" + dbMap.GetString("port") + ")/" + dbMap.GetString("database") + "?charset=utf8mb4&timeout=30s"
dbConfig := &dbs.Config{
DBs: map[string]*dbs.DBConfig{
"prod": {
Driver: "mysql",
Dsn: dsn,
Prefix: "edge",
}},
}
dbConfigData, err := yaml.Marshal(dbConfig)
if err != nil {
this.Fail("生成数据库配置失败:" + err.Error())
}
err = ioutil.WriteFile(apiNodeDir+"/configs/db.yaml", dbConfigData, 0666)
if err != nil {
this.Fail("保存数据库配置失败:" + err.Error())
}
err = ioutil.WriteFile(Tea.ConfigFile("/api_db.yaml"), dbConfigData, 0666)
if err != nil {
this.Fail("保存数据库配置失败:" + err.Error())
}
// 开始安装
var resultMap = maps.Map{}
{
cmd := exec.Command(apiNodeDir+"/bin/edge-api", "setup", "-api-node-protocol=http", "-api-node-host=\""+apiNodeMap.GetString("newHost")+"\"", "-api-node-port=\""+apiNodeMap.GetString("newPort")+"\"")
output := bytes.NewBuffer([]byte{})
cmd.Stdout = output
err = cmd.Run()
if err != nil {
this.Fail("安装失败:" + err.Error())
}
resultData := output.Bytes()
err = json.Unmarshal(resultData, &resultMap)
if err != nil {
this.Fail("安装节点时返回数据错误:" + err.Error() + "(" + string(resultData) + ")")
}
if !resultMap.GetBool("isOk") {
this.Fail("节点安装错误:" + resultMap.GetString("error"))
}
}
// 启动API节点
{
cmd := exec.Command(apiNodeDir + "/bin/edge-api")
err = cmd.Start()
if err != nil {
this.Fail("API节点启动失败" + err.Error())
}
}
// 写入API节点配置完成安装
apiConfig := &configs.APIConfig{
RPC: struct {
Endpoints []string `yaml:"endpoints"`
}{
Endpoints: []string{"http://" + apiNodeMap.GetString("newHost") + ":" + apiNodeMap.GetString("newPort")},
},
NodeId: resultMap.GetString("adminNodeId"),
Secret: resultMap.GetString("adminNodeSecret"),
}
// 设置管理员
client, err := rpc.NewRPCClient(apiConfig)
if err != nil {
this.FailField("oldHost", "测试API节点时出错请检查配置错误信息"+err.Error())
}
ctx := client.Context(0)
for i := 0; i < 3; i++ {
_, err = client.AdminRPC().CreateOrUpdateAdmin(ctx, &pb.CreateOrUpdateAdminRequest{
Username: adminMap.GetString("username"),
Password: adminMap.GetString("password"),
})
// 这里我们尝试多次是为了当代API节点启动完毕
if err != nil {
time.Sleep(1 * time.Second)
}
}
if err != nil {
this.Fail("设置管理员账号出错:" + err.Error())
}
err = apiConfig.WriteFile(Tea.ConfigFile("api.yaml"))
if err != nil {
this.Fail("保存配置失败,原因:" + err.Error())
}
this.Success()
} else if mode == "old" {
// 构造RPC
apiConfig := &configs.APIConfig{
RPC: struct {
Endpoints []string `yaml:"endpoints"`
}{
Endpoints: []string{apiNodeMap.GetString("oldProtocol") + "://" + apiNodeMap.GetString("oldHost") + ":" + apiNodeMap.GetString("oldPort")},
},
NodeId: apiNodeMap.GetString("oldNodeId"),
Secret: apiNodeMap.GetString("oldNodeSecret"),
}
client, err := rpc.NewRPCClient(apiConfig)
if err != nil {
this.FailField("oldHost", "测试API节点时出错请检查配置错误信息"+err.Error())
}
// 设置管理员
ctx := client.APIContext(0)
_, err = client.AdminRPC().CreateOrUpdateAdmin(ctx, &pb.CreateOrUpdateAdminRequest{
Username: adminMap.GetString("username"),
Password: adminMap.GetString("password"),
})
if err != nil {
this.Fail("设置管理员账号出错:" + err.Error())
}
// 写入API节点配置完成安装
err = apiConfig.WriteFile(Tea.ConfigFile("api.yaml"))
if err != nil {
this.Fail("保存配置失败,原因:" + err.Error())
}
// 成功
this.Success()
} else {
this.Fail("错误的API节点模式'" + mode + "'")
}
}

View File

@@ -0,0 +1,38 @@
package setup
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
"strings"
)
type ValidateAdminAction struct {
actionutils.ParentAction
}
func (this *ValidateAdminAction) RunPost(params struct {
AdminUsername string
AdminPassword string
AdminPassword2 string
Must *actions.Must
}) {
params.Must.
Field("adminUsername", params.AdminUsername).
Require("请输入登录用户名").
Match(`^[a-zA-Z0-9_]+$`, "用户名中只能包含英文、数字或下划线").
Field("adminPassword", params.AdminPassword).
Require("请输入登录密码").
Match(`^[a-zA-Z0-9_]+$`, "密码中只能包含英文、数字或下划线").
Field("adminPassword2", params.AdminPassword2).
Require("请输入确认密码").
Equal(params.AdminPassword, "两次输入的密码不一致")
this.Data["admin"] = maps.Map{
"username": params.AdminUsername,
"password": params.AdminPassword,
"passwordMask": strings.Repeat("*", len(params.AdminPassword)),
}
this.Success()
}

View File

@@ -0,0 +1,105 @@
package setup
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configs"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
"net"
"strings"
)
type ValidateApiAction struct {
actionutils.ParentAction
}
func (this *ValidateApiAction) RunPost(params struct {
Mode string
NewPort string
NewHost string
OldProtocol string
OldHost string
OldPort string
OldNodeId string
OldNodeSecret string
Must *actions.Must
}) {
params.OldNodeId = strings.Trim(params.OldNodeId, "\"' ")
params.OldNodeSecret = strings.Trim(params.OldNodeSecret, "\"' ")
this.Data["apiNode"] = maps.Map{
"mode": params.Mode,
"newPort": params.NewPort,
"newHost": params.NewHost,
"oldProtocol": params.OldProtocol,
"oldHost": params.OldHost,
"oldPort": params.OldPort,
"oldNodeId": params.OldNodeId,
"oldNodeSecret": params.OldNodeSecret,
}
if params.Mode == "new" {
params.Must.
Field("newPort", params.NewPort).
Require("请输入节点端口").
Match(`^\d+$`, "节点端口只能是数字").
MinLength(4, "请输入4位以上的数字").
MaxLength(5, "请输入5位以下的数字")
newPort := types.Int(params.NewPort)
if newPort < 1024 {
this.FailField("newPort", "端口号不能小于1024")
}
if newPort > 65534 {
this.FailField("newPort", "端口号不能大于65534")
}
conn, err := net.Dial("tcp", "127.0.0.1:"+params.NewPort)
if err == nil {
_ = conn.Close()
this.FailField("newPort", "此端口已经被别的服务占用,请换一个")
}
params.Must.
Field("newHost", params.NewHost).
Require("请输入节点主机地址")
this.Success()
return
}
// 使用已有的API节点
params.Must.
Field("oldHost", params.OldHost).
Require("请输入主机地址").
Field("oldPort", params.OldPort).
Require("请输入服务端口").
Match(`^\d+$`, "服务端口只能是数字").
Field("oldNodeId", params.OldNodeId).
Require("请输入节点nodeId").
Field("oldNodeSecret", params.OldNodeSecret).
Require("请输入节点secret")
client, err := rpc.NewRPCClient(&configs.APIConfig{
RPC: struct {
Endpoints []string `yaml:"endpoints"`
}{
Endpoints: []string{params.OldProtocol + "://" + params.OldHost + ":" + params.OldPort},
},
NodeId: params.OldNodeId,
Secret: params.OldNodeSecret,
})
if err != nil {
this.FailField("oldHost", "测试API节点时出错请检查配置错误信息"+err.Error())
}
_, err = client.APINodeRPC().FindCurrentAPINodeVersion(client.APIContext(0), &pb.FindCurrentAPINodeVersionRequest{})
if err != nil {
this.FailField("oldHost", "无法连接此API节点错误信息"+err.Error())
}
this.Success()
}

View File

@@ -0,0 +1,78 @@
package setup
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
_ "github.com/go-sql-driver/mysql"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/dbs"
"github.com/iwind/TeaGo/maps"
"strings"
)
type ValidateDbAction struct {
actionutils.ParentAction
}
func (this *ValidateDbAction) RunPost(params struct {
Host string
Port string
Database string
Username string
Password string
Must *actions.Must
}) {
params.Must.
Field("host", params.Host).
Require("请输入主机地址").
Match(`^[\w\.-]+$`, "主机地址中不能包含特殊字符").
Field("port", params.Port).
Require("请输入端口").
Match(`^\d+$`, "端口中只能包含数字").
Field("database", params.Database).
Require("请输入数据库名称").
Match(`^[\w\.-]+$`, "数据库名称中不能包含特殊字符").
Field("username", params.Username).
Require("请输入连接数据库的用户名").
Match(`^[\w\.-]+$`, "用户名中不能包含特殊字符")
// 测试连接
db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
Driver: "mysql",
Dsn: params.Username + ":" + params.Password + "@tcp(" + params.Host + ":" + params.Port + ")/" + params.Database,
Prefix: "",
})
if err != nil {
this.Fail("数据库信息错误:" + err.Error())
}
err = db.Raw().Ping()
if err != nil {
// 是否是数据库不存在
if strings.Contains(err.Error(), "Error 1049") {
db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
Driver: "mysql",
Dsn: params.Username + ":" + params.Password + "@tcp(" + params.Host + ":" + params.Port + ")/",
Prefix: "",
})
_, err = db.Exec("CREATE DATABASE `" + params.Database + "`")
if err != nil {
this.Fail("尝试创建数据库失败:" + err.Error())
}
} else {
this.Fail("无法连接到数据库,请检查配置:" + err.Error())
}
}
this.Data["db"] = maps.Map{
"host": params.Host,
"port": params.Port,
"database": params.Database,
"username": params.Username,
"password": params.Password,
"passwordMask": strings.Repeat("*", len(params.Password)),
}
this.Success()
}

View File

@@ -1,6 +1,7 @@
package ui
import (
"compress/gzip"
"github.com/iwind/TeaGo"
"github.com/iwind/TeaGo/actions"
)
@@ -12,7 +13,7 @@ func init() {
Get("/download", new(DownloadAction)).
// 以下的需要压缩
Helper(new(actions.Gzip)).
Helper(&actions.Gzip{Level: gzip.BestCompression}).
Get("/components.js", new(ComponentsAction)).
EndAll()
})

View File

@@ -3,6 +3,7 @@ package helpers
import (
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
nodes "github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeAdmin/internal/setup"
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
@@ -23,6 +24,12 @@ func NewUserMustAuth() *UserMustAuth {
func (this *UserMustAuth) BeforeAction(actionPtr actions.ActionWrapper, paramName string) (goNext bool) {
var action = actionPtr.Object()
// 检查系统是否已经配置过
if !setup.IsConfigured() {
action.RedirectURL("/setup")
return
}
var session = action.Session()
var adminId = session.GetInt("adminId")
if adminId <= 0 {
@@ -33,6 +40,7 @@ func (this *UserMustAuth) BeforeAction(actionPtr actions.ActionWrapper, paramNam
// 检查用户是否存在
rpc, err := nodes.SharedRPC()
if err != nil {
action.WriteString("setup rpc error: " + err.Error())
utils.PrintError(err)
return false
}
@@ -40,7 +48,7 @@ func (this *UserMustAuth) BeforeAction(actionPtr actions.ActionWrapper, paramNam
rpcResp, err := rpc.AdminRPC().CheckAdminExists(rpc.Context(0), &pb.CheckAdminExistsRequest{AdminId: int64(adminId)})
if err != nil {
utils.PrintError(err)
actionPtr.Object().WriteString(teaconst.ErrServer)
action.WriteString(teaconst.ErrServer)
return false
}

View File

@@ -71,5 +71,6 @@ import (
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/profile"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/security"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/upgrade"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/setup"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/ui"
)

View File

@@ -138,8 +138,8 @@ window.teaweb = {
if (message.length > 30) {
width = "30em";
}
Swal.fire({
text: message,
let config = {
confirmButtonText: "确定",
buttonsStyling: false,
icon: "success",
@@ -154,9 +154,19 @@ window.teaweb = {
setTimeout(function () {
callback();
});
} else if (typeof (callback) == "string") {
window.location = callback
}
}
});
}
if (message.startsWith("html:")) {
config.html = message.substring(5)
} else {
config.text = message
}
Swal.fire(config);
},
successToast: function (message, timeout) {
if (timeout == null) {

View File

@@ -130,7 +130,7 @@ p.comment,
div.comment {
color: rgba(0, 0, 0, 0.3);
padding-top: 0.4em;
font-size: 0.9em;
font-size: 1em;
}
p.comment em,
div.comment em {

File diff suppressed because one or more lines are too long

View File

@@ -23,7 +23,7 @@
<!-- 顶部导航 -->
<div class="ui menu top-nav blue inverted small borderless" v-cloak="">
<a href="/" class="item">
<i class="ui icon leaf"></i> &nbsp; {{teaName}}管理员系统 &nbsp;
<i class="ui icon leaf"></i> &nbsp; {{teaName}}管理员系统&nbsp;<sup>v{{teaVersion}}</sup> &nbsp;
</a>
<div class="right menu">

View File

@@ -64,7 +64,7 @@ tbody {
p.comment, div.comment {
color: rgba(0, 0, 0, 0.3);
padding-top: 0.4em;
font-size: 0.9em;
font-size: 1em;
}
p.comment em, div.comment em {

View File

@@ -26,7 +26,7 @@
<td>外部访问地址 *</td>
<td>
<api-node-addresses-box :v-name="'accessAddrsJSON'"></api-node-addresses-box>
<p class="comment">外部访问API节点的网络地址。</p>
<p class="comment">边缘节点和管理平台等外部节点访问API节点的网络地址。</p>
</td>
</tr>

View File

@@ -29,7 +29,7 @@
<td>外部访问地址 *</td>
<td>
<api-node-addresses-box :v-name="'accessAddrsJSON'" :v-addrs="node.accessAddrs"></api-node-addresses-box>
<p class="comment">外部访问API节点的网络地址。</p>
<p class="comment">边缘节点和管理平台等外部节点访问API节点的网络地址。</p>
</td>
</tr>

View File

@@ -2,7 +2,7 @@
{$template "menu"}
<p class="ui message error" v-if="error.length > 0">{{error}}</p>
<div v-show="error.length == 0 && dbConfig != null">
<div v-if="error.length == 0 && dbConfig != null">
<table class="ui table selectable definition">
<tr>
<td class="title">主机地址</td>

View File

View File

@@ -0,0 +1 @@
undefined

View File

@@ -0,0 +1,67 @@
.install-box {
@width: 50em;
width: @width;
position: fixed;
left: 50%;
margin-left: -@width/2;
top: 1em;
bottom: 1em;
overflow-y: auto;
.button.margin {
margin-top: 1em;
}
.button.primary {
float: right;
}
table {
td.title {
width: 10em;
}
}
.radio {
margin-right: 1em;
label {
cursor: pointer !important;
font-size: 0.9em !important;
}
}
h3 {
font-weight: normal;
}
.content-box {
overflow-y: auto;
position: fixed;
top: 5em;
bottom: 5em;
left: 50%;
width: @width;
padding-right: 1em;
margin-left: -@width/2;
z-index: 1;
}
.content-box::-webkit-scrollbar {
width: 4px;
}
.button-group {
position: fixed;
left: 50%;
margin-left: -@width/2;
z-index: 1;
width: @width;
bottom: 1em;
}
}
.install-box::-webkit-scrollbar {
width: 4px;
}

View File

@@ -0,0 +1,54 @@
.install-box {
width: 50em;
position: fixed;
left: 50%;
margin-left: -25em;
top: 1em;
bottom: 1em;
overflow-y: auto;
}
.install-box .button.margin {
margin-top: 1em;
}
.install-box .button.primary {
float: right;
}
.install-box table td.title {
width: 10em;
}
.install-box .radio {
margin-right: 1em;
}
.install-box .radio label {
cursor: pointer !important;
font-size: 0.9em !important;
}
.install-box h3 {
font-weight: normal;
}
.install-box .content-box {
overflow-y: auto;
position: fixed;
top: 5em;
bottom: 5em;
left: 50%;
width: 50em;
padding-right: 1em;
margin-left: -25em;
z-index: 1;
}
.install-box .content-box::-webkit-scrollbar {
width: 4px;
}
.install-box .button-group {
position: fixed;
left: 50%;
margin-left: -25em;
z-index: 1;
width: 50em;
bottom: 1em;
}
.install-box::-webkit-scrollbar {
width: 4px;
}
/*# sourceMappingURL=index.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["@install.less"],"names":[],"mappings":"AAAA;EAGC,WAAA;EACA,eAAA;EACA,SAAA;EACA,kBAAA;EACA,QAAA;EACA,WAAA;EACA,gBAAA;;AATD,YAWC,QAAO;EACN,eAAA;;AAZF,YAeC,QAAO;EACN,YAAA;;AAhBF,YAmBC,MACC,GAAE;EACD,WAAA;;AArBH,YAyBC;EACC,iBAAA;;AA1BF,YAyBC,OAGC;EACC,0BAAA;EACA,2BAAA;;AA9BH,YAkCC;EACC,mBAAA;;AAnCF,YAsCC;EACC,gBAAA;EACA,eAAA;EACA,QAAA;EACA,WAAA;EACA,SAAA;EACA,WAAA;EACA,kBAAA;EACA,kBAAA;EACA,UAAA;;AA/CF,YAkDC,aAAY;EACX,UAAA;;AAnDF,YAsDC;EACC,eAAA;EACA,SAAA;EACA,kBAAA;EACA,UAAA;EACA,WAAA;EACA,WAAA;;AAIF,YAAY;EACX,UAAA","file":"index.css"}

View File

@@ -0,0 +1,346 @@
<!doctype html>
<html lang="zh">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<link rel="shortcut icon" href="/images/favicon.png"/>
<title>安装Edge管理系统</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="/ui/components.js"></script>
<link rel="stylesheet" href="/_/@default/@layout.css"/>
</head>
<body>
<div class="install-box">
<!-- 步骤列表 -->
<div class="ui steps fluid small">
<div class="ui step" :class="{active: step == STEP_INTRO}">
<div class="content">
<div :class="{title: step == STEP_INTRO}">1. 介绍</div>
</div>
</div>
<div class="ui step" :class="{active: step == STEP_API}">
<div class="content">
<div :class="{title: step == STEP_API}">2. 设置API节点</div>
</div>
</div>
<div class="ui step" :class="{active: step == STEP_DB}">
<div class="content">
<div :class="{title: step == STEP_DB}">3. 设置MySQL数据库</div>
</div>
</div>
<div class="ui step" :class="{active: step == STEP_ADMIN}">
<div class="content">
<div :class="{title: step == STEP_ADMIN}">4. 设置管理员账号</div>
</div>
</div>
<div class="ui step" :class="{active: step == STEP_FINISH}">
<div class="content">
<div :class="{title: step == STEP_FINISH}">5. 完成安装</div>
</div>
</div>
</div>
<!-- 介绍 -->
<div v-show="step == STEP_INTRO">
<div>感谢你选择使用<strong>Edge</strong>集群服务系统,下面让我们一起开始配置系统。</div>
<div class="margin">在这之前如果你还没有可用的MySQL数据库请先安装数据库再进行。</div>
<button class="ui button primary" style="margin-top: 10em" type="button" @click.prevent="goIntroNext()">开始<i class="icon long arrow right"></i></button>
</div>
<!-- 设置API节点 -->
<div v-show="step == STEP_API">
<div class="ui message"><strong>API节点</strong>是管理系统和数据库之间通讯的桥梁新安装时可以先使用自动启动的API节点等系统变得庞大时再扩展新的API节点。</div>
<form class="ui form" data-tea-action=".validateApi" data-tea-success="apiSuccess" data-tea-before="apiSubmit" data-tea-done="apiDone">
<table class="ui table definition selectable">
<tr>
<td class="title">API节点类型</td>
<td>
<div class="ui radio checkbox">
<input type="radio" name="mode" value="new" id="auto-start-api-node" v-model="apiNodeMode"/>
<label for="auto-start-api-node">自动启动新API节点</label>
</div>
<div class="ui radio checkbox">
<input type="radio" name="mode" value="old" id="use-old-api-node" v-model="apiNodeMode"/>
<label for="use-old-api-node">使用已安装节点</label>
</div>
</td>
</tr>
<tbody v-show="apiNodeMode == 'new'">
<tr>
<td>节点端口 *</td>
<td>
<input type="text" name="newPort" style="width:6em" maxlength="5" v-model="newAPINodePort"/>
<p class="comment">选一个在1024-65535之间并且没有正在使用的端口作为要启动的节点端口。</p>
</td>
</tr>
<tr>
<td>节点主机地址</td>
<td>
<div v-if="serverIPs.length > 0">
<select name="newHost" class="ui dropdown auto-width">
<option v-for="ip in serverIPs" :value="ip">{{ip}}</option>
</select>
</div>
<div v-else="">
<input type="text" name="newHost" value=""/>
</div>
<p class="comment">其他节点访问此API节点的主机地址可以是IP或者域名第一次安装时通常是<strong>当前服务器</strong>的IP地址。后期可以修改这个地址。</p>
</td>
</tr>
</tbody>
<tbody v-show="apiNodeMode == 'old'">
<tr>
<td>节点协议 *</td>
<td>
<select class="ui dropdown auto-width" name="oldProtocol">
<option value="http">HTTP</option>
<option value="https">HTTPS</option>
</select>
<p class="comment">API节点使用的协议。</p>
</td>
</tr>
<tr>
<td>主机地址 *</td>
<td>
<input type="text" name="oldHost" maxlength="100" style="width:20em"/>
<p class="comment">API节点所在主机地址。</p>
</td>
</tr>
<tr>
<td>服务端口 *</td>
<td>
<input type="text" name="oldPort" style="width:6em" maxlength="5"/>
<p class="comment">API节点启动的端口。</p>
</td>
</tr>
<tr>
<td>节点nodeId *</td>
<td>
<input type="text" name="oldNodeId" maxlength="100"/>
<p class="comment">在节点的配置文件中<code-label>configs/api.yaml</code-label>中获取,不需要带双引号。</p>
</td>
</tr>
<tr>
<td>节点secret *</td>
<td>
<input type="password" name="oldNodeSecret" maxlength="100"/>
<p class="comment">在节点的配置文件中<code-label>configs/api.yaml</code-label>中获取,不需要带双引号。</p>
</td>
</tr>
</tbody>
</table>
<button class="ui button" type="button" @click.prevent="goBackIntro"><i class="icon long arrow left"></i>上一步</button> &nbsp;
<button class="ui button primary" type="submit" v-if="!apiRequesting">下一步<i class="icon long arrow right"></i></button>
<button class="ui button primary" type="button" v-if="apiRequesting">提交中</button>
</form>
</div>
<!-- 设置数据库 -->
<div v-show="step == STEP_DB">
<div v-show="apiNodeMode == 'new'">
<form class="ui form" data-tea-action=".validateDb" data-tea-success="dbSuccess" data-tea-before="dbSubmit" data-tea-done="dbDone">
<table class="ui table definition selectable">
<tr>
<td class="title">MySQL主机地址 *</td>
<td>
<input type="text" name="host" maxlength="100" placeholder="比如 192.168.1.100" style="width:16em" ref="dbHost"/>
</td>
</tr>
<tr>
<td>数据库连接端口 *</td>
<td>
<input type="text" name="port" maxlength="5" placeholder="比如 3306" style="width:7em"/>
</td>
</tr>
<tr>
<td>数据库名称 *</td>
<td>
<input type="text" name="database" value="edges" style="width:16em"/>
<p class="comment">请事先创建好此数据库,如果不存在,则会尝试自动创建。</p>
</td>
</tr>
<tr>
<td>连接用户名 *</td>
<td>
<input type="text" name="username" style="width:16em" maxlength="100"/>
<p class="comment">此用户需要可以在数据库中有操作数据和创建数据表的权限。</p>
</td>
</tr>
<tr>
<td>连接密码</td>
<td>
<input type="password" name="password" style="width:16em" maxlength="100"/>
<p class="comment">连接数据库所需密码,没有密码的话就不需要填写。</p>
</td>
</tr>
</table>
<button class="ui button" type="button" @click.prevent="goBackAPI"><i class="icon long arrow left"></i>上一步</button> &nbsp;
<button class="ui button primary" v-if="!dbRequesting" type="submit">下一步<i class="icon long arrow right"></i></button>
<button class="ui button primary" v-if="dbRequesting" type="button">提交中...</button>
</form>
</div>
<div v-show="apiNodeMode == 'old'">
<div style="margin-bottom: 2em">你选择使用了已安装节点,无需重新配置数据库。</div>
<button class="ui button margin" type="button" @click.prevent="goBackAPI"><i class="icon long arrow left"></i>上一步</button> &nbsp;
<button class="ui button primary margin" type="button" @click.prevent="goDBNext()">下一步<i class="icon long arrow right"></i></button>
</div>
</div>
<!-- 设置管理员账号 -->
<div v-show="step == STEP_ADMIN">
<form class="ui form" data-tea-action=".validateAdmin" data-tea-success="adminSuccess">
<table class="ui table definition selectable">
<tr>
<td class="title">登录用户名 *</td>
<td>
<input type="text" name="adminUsername" style="width:16em" maxlength="100" value="admin"/>
<p class="comment">只能是英文、数字和下划线的组合。</p>
</td>
</tr>
<tr>
<td>登录密码 *</td>
<td>
<input type="password" name="adminPassword" maxlength="100" style="width:16em" v-model="adminPassword" v-show="!adminPasswordVisible"/>
<input type="text" value="" v-model="adminPassword" style="width:16em" v-show="adminPasswordVisible"/>
<p class="comment">只能是英文、数字和下划线的组合 <a href="" title="显示明文密码" @click.prevent="showAdminPassword"><i class="icon eye grey"></i></a></p>
</td>
</tr>
<tr>
<td>确认密码 *</td>
<td>
<input type="password" name="adminPassword2" maxlength="100" style="width:16em" v-model="adminPassword2" v-show="!adminPasswordVisible"/>
<input type="text" value="" v-model="adminPassword2" style="width:16em" v-show="adminPasswordVisible"/>
<p class="comment">再次输入密码以便于确认 <a href="" title="显示明文密码" @click.prevent="showAdminPassword"><i class="icon eye grey"></i></a></p>
</td>
</tr>
</table>
<button class="ui button margin" type="button" @click.prevent="goBackDB"><i class="icon long arrow left"></i>上一步</button> &nbsp;
<button class="ui button primary margin" type="submit">下一步<i class="icon long arrow right"></i></button>
</form>
</div>
<!-- 完成安装 -->
<div v-show="step == STEP_FINISH">
<form class="ui form" data-tea-action=".install" data-tea-success="finishSuccess" data-tea-before="finishSubmit" data-tea-done="finishDone">
<input type="hidden" name="apiNodeJSON" :value="JSON.stringify(apiNodeInfo)"/>
<input type="hidden" name="dbJSON" :value="JSON.stringify(dbInfo)"/>
<input type="hidden" name="adminJSON" :value="JSON.stringify(adminInfo)"/>
<div class="content-box">
<h3>API节点信息</h3>
<table class="ui table definition selectable">
<tr>
<td class="title">API节点类型</td>
<td>
<span v-if="apiNodeInfo.mode == 'new'">自动启动新API节点</span>
<span v-if="apiNodeInfo.mode == 'old'">使用已安装节点</span>
</td>
</tr>
<tbody v-show="apiNodeInfo.mode == 'new'">
<tr>
<td>节点端口 *</td>
<td>
{{apiNodeInfo.newPort}}
</td>
</tr>
<tr>
<td>节点主机地址</td>
<td>
{{apiNodeInfo.newHost}}
</td>
</tr>
</tbody>
<tbody v-show="apiNodeInfo.mode == 'old'">
<tr>
<td>节点协议 *</td>
<td>
{{apiNodeInfo.oldProtocol}}
</td>
</tr>
<tr>
<td>主机地址 *</td>
<td>
{{apiNodeInfo.oldHost}}
</td>
</tr>
<tr>
<td>服务端口 *</td>
<td>
{{apiNodeInfo.oldPort}}
</td>
</tr>
</tbody>
</table>
<h3 v-if="apiNodeMode == 'new'">数据库信息</h3>
<table class="ui table definition selectable" v-if="apiNodeMode == 'new'">
<tr>
<td class="title">MySQL主机地址 *</td>
<td>
{{dbInfo.host}}
</td>
</tr>
<tr>
<td>数据库连接端口 *</td>
<td>
{{dbInfo.port}}
</td>
</tr>
<tr>
<td>数据库名称 *</td>
<td>
{{dbInfo.database}}
</td>
</tr>
<tr>
<td>连接用户名 *</td>
<td>
{{dbInfo.username}}
</td>
</tr>
<tr>
<td>连接密码</td>
<td>
<span v-if="dbInfo.passwordMask != null && dbInfo.passwordMask.length > 0">{{dbInfo.passwordMask}}</span>
<span v-else class="disabled">未填入密码</span>
</td>
</tr>
</table>
<h3>管理员信息</h3>
<table class="ui table definition selectable">
<tr>
<td class="title">登录用户名</td>
<td>{{adminInfo.username}}</td>
</tr>
<tr>
<td>登录密码</td>
<td>{{adminInfo.passwordMask}}</td>
</tr>
</table>
</div>
<div class="button-group">
<div class="ui divider"></div>
<button class="ui button" type="button" @click.prevent="goBackAdmin"><i class="icon long arrow left"></i>上一步</button>
<button class="ui button primary" type="submit" v-if="!isInstalling">确认并开始安装</button>
<button class="ui button primary" type="button" v-if="isInstalling">安装中...</button>
</div>
</form>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,106 @@
Tea.context(function () {
this.STEP_INTRO = "intro"
this.STEP_API = "api"
this.STEP_DB = "db"
this.STEP_ADMIN = "admin"
this.STEP_FINISH = "finish"
this.step = this.STEP_INTRO
// 介绍
this.goIntroNext = function () {
this.step = this.STEP_API
}
// API节点
this.apiNodeInfo = {}
this.apiNodeMode = "new"
this.newAPINodePort = "8001"
this.apiRequesting = false
this.apiSubmit = function () {
this.apiRequesting = true
}
this.apiDone = function () {
this.apiRequesting = false
}
this.apiSuccess = function (resp) {
this.step = this.STEP_DB
this.apiNodeInfo = resp.data.apiNode
if (this.apiNodeMode == "new") {
this.$delay(function () {
this.$refs.dbHost.focus()
}, 200)
}
}
this.goBackIntro = function () {
this.step = this.STEP_INTRO
}
// 数据库
this.dbInfo = {}
this.dbRequesting = false
this.dbSubmit = function () {
this.dbRequesting = true
}
this.dbSuccess = function (resp) {
this.step = this.STEP_ADMIN
this.dbInfo = resp.data.db
}
this.dbDone = function () {
this.dbRequesting = false
}
this.goBackAPI = function () {
this.step = this.STEP_API
}
this.goDBNext = function () {
this.step = this.STEP_ADMIN
}
// 管理员
this.goBackDB = function () {
this.step = this.STEP_DB
}
this.adminInfo = {}
this.adminPassword = ""
this.adminPassword2 = ""
this.adminPasswordVisible = false
this.showAdminPassword = function () {
this.adminPasswordVisible = !this.adminPasswordVisible
// TODO 切换密码显示的时候应该focus输入框
}
this.adminSuccess = function (resp) {
this.step = this.STEP_FINISH
this.adminInfo = resp.data.admin
}
// 结束
this.goBackAdmin = function () {
this.step = this.STEP_ADMIN
}
this.isInstalling = false
this.finishSubmit = function () {
this.isInstalling = true
}
this.finishDone = function () {
this.isInstalling = false
}
this.finishSuccess = function () {
teaweb.success("html:恭喜你!安装完成!<br/>请记住你创建的管理员账号,现在跳转到登录界面。", "/")
}
})

View File

@@ -0,0 +1,2 @@
@import "@install";