Compare commits

...

44 Commits

Author SHA1 Message Date
刘祥超
939e5999ca 优化细节 2021-10-30 22:33:56 +08:00
刘祥超
1f91e57d56 增加套餐相关代码 2021-10-29 14:02:01 +08:00
刘祥超
81e749dc60 支持gif转webp 2021-10-29 12:21:52 +08:00
刘祥超
241b2afda8 Update .gitignore 2021-10-26 20:43:59 +08:00
刘祥超
94cc29f227 删除不必要的文件 2021-10-26 20:43:47 +08:00
刘祥超
6d6659eee1 WAF增加显示网页动作 2021-10-25 19:40:28 +08:00
刘祥超
5220be0775 优化代码 2021-10-25 19:01:56 +08:00
刘祥超
07ebbf0863 WAF模板中有新的规则时,可以在界面上收到提醒并点击加入 2021-10-25 12:02:03 +08:00
刘祥超
b60c767fc5 创建网站服务时增加缓存、WAF、从上级代理中读取IP等选项 2021-10-25 09:06:23 +08:00
刘祥超
371c3b78c3 DNS记录名支持下划线 2021-10-25 09:05:26 +08:00
刘祥超
6a3aa219d5 优化HTTP客户端IP配置交互 2021-10-22 14:40:39 +08:00
刘祥超
df586ddfdd 优化代码 2021-10-22 13:57:04 +08:00
刘祥超
f3b2bbfec0 删除不需要的文件 2021-10-22 13:20:02 +08:00
刘祥超
3ea2114798 IP名单列表可以搜索关键词 2021-10-22 12:38:52 +08:00
刘祥超
809cf70e0e 可以在IP名单中搜索IP 2021-10-22 12:19:02 +08:00
刘祥超
390619535f 实现单个服务的带宽限制(商业版) 2021-10-21 17:10:08 +08:00
刘祥超
3392ac1fa8 优化交互 2021-10-20 09:59:08 +08:00
刘祥超
b09d94abbe 网站服务--访问日志增加服务链接 2021-10-19 16:32:08 +08:00
刘祥超
03ac01d21f 健康检查支持UserAgent和是否基础请求设置 2021-10-19 16:31:45 +08:00
刘祥超
12b1d785e5 增加防盗链规则参数 2021-10-19 11:38:56 +08:00
刘祥超
71c58e9d2e WAF阻止动作增加封锁范围 2021-10-18 20:09:06 +08:00
刘祥超
13c2997a52 内容压缩支持对已压缩内容重新压缩 2021-10-18 16:49:19 +08:00
刘祥超
47b840cac9 优化文字提示 2021-10-18 16:49:04 +08:00
刘祥超
3f7f243f50 默认的内容压缩算法从gzip改为brotli 2021-10-18 11:59:50 +08:00
刘祥超
1d1e83b18d 增加PURGE某个URL缓存功能 2021-10-17 20:22:57 +08:00
刘祥超
6f3a602c76 优化分页条数修改/在弹窗下不运行某些任务 2021-10-16 12:45:56 +08:00
刘祥超
afb7a4c6a7 修复选择集群弹窗无法修改分页条数的问题 2021-10-16 12:45:25 +08:00
刘祥超
d0c950d4ca 优化界面 2021-10-16 12:06:55 +08:00
刘祥超
63ee7d5211 支持任意域名通过CNAME访问服务(开启选项后)/可以重新生成服务CNAME 2021-10-16 12:03:21 +08:00
刘祥超
6b0d875745 优化界面显示 2021-10-16 10:29:14 +08:00
刘祥超
a47a9b9c0c 删除CodeMirror中没用的代码 2021-10-16 10:26:14 +08:00
刘祥超
27040a3e5c 节点运行日志增加本页已读 2021-10-15 13:05:02 +08:00
刘祥超
c7a8a40e22 运行日志显示未读的日志数量 2021-10-15 12:54:23 +08:00
刘祥超
408de6af63 优化文字提示 2021-10-15 09:42:55 +08:00
刘祥超
8a324afaa1 数据看板增加事件列表(商业版) 2021-10-14 17:29:30 +08:00
刘祥超
54bc4cede0 修复无法编译amd64以外架构的Bug 2021-10-13 18:06:38 +08:00
刘祥超
4cfbea80b0 支持PROXY Protocol 2021-10-12 20:18:29 +08:00
刘祥超
64cb8286bd 集群非上海时区的在列表里显示时区 2021-10-12 14:39:13 +08:00
刘祥超
3e92e0afc6 优化修改时区交互 2021-10-12 11:49:26 +08:00
刘祥超
8a91308280 可以在集群中指定节点时区 2021-10-12 11:43:53 +08:00
刘祥超
3da861d71e 选择线路的时候关键词可以搜索域名 2021-10-12 08:41:02 +08:00
刘祥超
3566e18e99 WebP压缩支持ico 2021-10-11 14:51:50 +08:00
刘祥超
6e608e627a WebP默认mimeTypes从image/*改为image/png等 2021-10-11 13:56:51 +08:00
刘祥超
65f7fb979b 修改版本为0.3.3 2021-10-11 13:56:20 +08:00
112 changed files with 1911 additions and 912 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
*_plus.go
*-plus.sh
*_plus.html
*_plus.js
*@plus.js

View File

@@ -60,7 +60,7 @@ function build() {
# build
echo "building "${NAME}" ..."
env GOOS=$OS GOARCH=$GOARCH go build -tags $TAG -ldflags="-s -w" -o $DIST/bin/${NAME} $ROOT/../cmd/edge-admin/main.go
env GOOS=$OS GOARCH=$ARCH go build -tags $TAG -ldflags="-s -w" -o $DIST/bin/${NAME} $ROOT/../cmd/edge-admin/main.go
# delete hidden files
find $DIST -name ".DS_Store" -delete

4
go.mod
View File

@@ -10,9 +10,11 @@ require (
github.com/go-sql-driver/mysql v1.5.0
github.com/go-yaml/yaml v2.1.0+incompatible
github.com/google/go-cmp v0.5.6 // indirect
github.com/iwind/TeaGo v0.0.0-20210720011303-fc255c995afa
github.com/iwind/TeaGo v0.0.0-20211026123858-7de7a21cad24
github.com/iwind/gosock v0.0.0-20210722083328-12b2d66abec3
github.com/json-iterator/go v1.1.12 // indirect
github.com/miekg/dns v1.1.35
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
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

12
go.sum
View File

@@ -62,12 +62,16 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/iwind/TeaGo v0.0.0-20210628135026-38575a4ab060/go.mod h1:KU4mS7QNiZ7QWEuDBk1zw0/Q2LrAPZv3tycEFBsuUwc=
github.com/iwind/TeaGo v0.0.0-20210720011303-fc255c995afa h1:woN88uEmRRUNFD7pRZEtX9heDcjFn0ClMxjF5ButKow=
github.com/iwind/TeaGo v0.0.0-20210720011303-fc255c995afa/go.mod h1:KU4mS7QNiZ7QWEuDBk1zw0/Q2LrAPZv3tycEFBsuUwc=
github.com/iwind/TeaGo v0.0.0-20210831140440-a2a442471b13 h1:HuEJ5xJfujW1Q6rNDhOu5LQXEBB2qLPah3jYslT8Gz4=
github.com/iwind/TeaGo v0.0.0-20210831140440-a2a442471b13/go.mod h1:KU4mS7QNiZ7QWEuDBk1zw0/Q2LrAPZv3tycEFBsuUwc=
github.com/iwind/TeaGo v0.0.0-20211026123858-7de7a21cad24 h1:1cGulkD2SNJJRok5OKwyhP/Ddm+PgSWKOupn0cR36/A=
github.com/iwind/TeaGo v0.0.0-20211026123858-7de7a21cad24/go.mod h1:KU4mS7QNiZ7QWEuDBk1zw0/Q2LrAPZv3tycEFBsuUwc=
github.com/iwind/gosock v0.0.0-20210722083328-12b2d66abec3 h1:aBSonas7vFcgTj9u96/bWGILGv1ZbUSTLiOzcI1ZT6c=
github.com/iwind/gosock v0.0.0-20210722083328-12b2d66abec3/go.mod h1:H5Q7SXwbx3a97ecJkaS2sD77gspzE7HFUafBO0peEyA=
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/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 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -77,8 +81,12 @@ github.com/miekg/dns v1.1.35 h1:oTfOaDH+mZkdcgdIjH6yBajRGtIwcwcaR+rt23ZSrJs=
github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
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 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
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 h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
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=

View File

@@ -19,6 +19,7 @@ const (
AdminModuleCodeAdmin AdminModuleCode = "admin" // 系统用户
AdminModuleCodeUser AdminModuleCode = "user" // 平台用户
AdminModuleCodeFinance AdminModuleCode = "finance" // 财务
AdminModuleCodePlan AdminModuleCode = "plan" // 套餐
AdminModuleCodeLog AdminModuleCode = "log" // 日志
AdminModuleCodeSetting AdminModuleCode = "setting" // 设置
AdminModuleCodeCommon AdminModuleCode = "common" // 只要登录就可以访问的模块
@@ -203,6 +204,11 @@ func AllModuleMaps() []maps.Map {
"code": AdminModuleCodeFinance,
"url": "/finance",
},
{
"name": "套餐管理",
"code": AdminModuleCodePlan,
"url": "/plans",
},
{
"name": "日志审计",
"code": AdminModuleCodeLog,

View File

@@ -1,9 +1,9 @@
package teaconst
const (
Version = "0.3.2"
Version = "0.3.3"
APINodeVersion = "0.3.2"
APINodeVersion = "0.3.3"
ProductName = "Edge Admin"
ProcessName = "edge-admin"

View File

@@ -464,6 +464,14 @@ func (this *RPCClient) ServerStatBoardChartRPC() pb.ServerStatBoardChartServiceC
return pb.NewServerStatBoardChartServiceClient(this.pickConn())
}
func (this *RPCClient) PlanRPC() pb.PlanServiceClient {
return pb.NewPlanServiceClient(this.pickConn())
}
func (this *RPCClient) UserPlanRPC() pb.UserPlanServiceClient {
return pb.NewUserPlanServiceClient(this.pickConn())
}
// Context 构造Admin上下文
func (this *RPCClient) Context(adminId int64) context.Context {
ctx := context.Background()

View File

@@ -1,6 +1,7 @@
package utils
import (
"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
"github.com/miekg/dns"
)
@@ -16,13 +17,19 @@ func LookupCNAME(host string) (string, error) {
m.SetQuestion(host+".", dns.TypeCNAME)
m.RecursionDesired = true
r, _, err := c.Exchange(m, config.Servers[0]+":"+config.Port)
if err != nil {
return "", err
}
if len(r.Answer) == 0 {
return "", nil
}
return r.Answer[0].(*dns.CNAME).Target, nil
var lastErr error
for _, serverAddr := range config.Servers {
r, _, err := c.Exchange(m, configutils.QuoteIP(serverAddr)+":"+config.Port)
if err != nil {
lastErr = err
continue
}
if len(r.Answer) == 0 {
continue
}
return r.Answer[0].(*dns.CNAME).Target, nil
}
return "", lastErr
}

View File

@@ -19,6 +19,8 @@ type ParentAction struct {
actions.ActionObject
rpcClient *rpc.RPCClient
ctx context.Context
}
// Parent 可以调用自身的一个简便方法
@@ -117,6 +119,10 @@ func (this *ParentAction) RPC() *rpc.RPCClient {
// AdminContext 获取Context
func (this *ParentAction) AdminContext() context.Context {
if this.ctx != nil {
return this.ctx
}
if this.rpcClient == nil {
rpcClient, err := rpc.SharedRPC()
if err != nil {
@@ -125,7 +131,8 @@ func (this *ParentAction) AdminContext() context.Context {
}
this.rpcClient = rpcClient
}
return this.rpcClient.Context(this.AdminId())
this.ctx = this.rpcClient.Context(this.AdminId())
return this.ctx
}
// ViewData 视图里可以使用的数据

View File

@@ -19,6 +19,7 @@ func init() {
server.
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeNode)).
Helper(clusters.NewClusterHelper()).
Data("teaMenu", "clusters").
Prefix("/clusters/cluster").
Get("", new(IndexAction)).
Get("/nodes", new(NodesAction)).

View File

@@ -4,6 +4,7 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/oplogs"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/grants/grantutils"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
@@ -21,6 +22,7 @@ func (this *IndexAction) Init() {
func (this *IndexAction) RunGet(params struct {
ClusterId int64
}) {
// 基本信息
clusterResp, err := this.RPC().NodeClusterRPC().FindEnabledNodeCluster(this.AdminContext(), &pb.FindEnabledNodeClusterRequest{NodeClusterId: params.ClusterId})
if err != nil {
this.ErrorPage(err)
@@ -53,10 +55,20 @@ func (this *IndexAction) RunGet(params struct {
}
this.Data["grant"] = grantMap
// 时区
this.Data["timeZoneGroups"] = nodeconfigs.FindAllTimeZoneGroups()
this.Data["timeZoneLocations"] = nodeconfigs.FindAllTimeZoneLocations()
if len(cluster.TimeZone) == 0 {
cluster.TimeZone = nodeconfigs.DefaultTimeZoneLocation
}
this.Data["timeZoneLocation"] = nodeconfigs.FindTimeZoneLocation(cluster.TimeZone)
this.Data["cluster"] = maps.Map{
"id": cluster.Id,
"name": cluster.Name,
"installDir": cluster.InstallDir,
"timeZone": cluster.TimeZone,
}
this.Show()
@@ -68,6 +80,7 @@ func (this *IndexAction) RunPost(params struct {
Name string
GrantId int64
InstallDir string
TimeZone string
Must *actions.Must
}) {
@@ -83,6 +96,7 @@ func (this *IndexAction) RunPost(params struct {
Name: params.Name,
NodeGrantId: params.GrantId,
InstallDir: params.InstallDir,
TimeZone: params.TimeZone,
})
if err != nil {
this.ErrorPage(err)

View File

@@ -12,7 +12,8 @@ func NewClustersHelper() *ClustersHelper {
return &ClustersHelper{}
}
func (this *ClustersHelper) BeforeAction(action *actions.ActionObject) {
func (this *ClustersHelper) BeforeAction(actionPtr actions.ActionWrapper) {
var action = actionPtr.Object()
if action.Request.Method != http.MethodGet {
return
}

View File

@@ -12,6 +12,7 @@ func init() {
server.
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeNode)).
Helper(clusterutils.NewClustersHelper()).
Data("teaMenu", "clusters").
Data("teaSubMenu", "grant").
Prefix("/clusters/grants").

View File

@@ -121,6 +121,10 @@ func (this *IndexAction) RunGet(params struct {
this.ErrorPage(err)
}
if cluster.TimeZone == nodeconfigs.DefaultTimeZoneLocation {
cluster.TimeZone = ""
}
clusterMaps = append(clusterMaps, maps.Map{
"id": cluster.Id,
"name": cluster.Name,
@@ -132,6 +136,7 @@ func (this *IndexAction) RunGet(params struct {
"dnsName": cluster.DnsName,
"dnsDomainName": dnsDomainName,
"countServers": countServersResp.Count,
"timeZone": cluster.TimeZone,
})
}
}

View File

@@ -12,6 +12,7 @@ func init() {
server.
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeNode)).
Helper(clusterutils.NewClustersHelper()).
Data("teaMenu", "clusters").
Prefix("/clusters").
Get("", new(IndexAction)).
GetPost("/create", new(CreateAction)).

View File

@@ -2,6 +2,7 @@ package logs
import (
"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/maps"
timeutil "github.com/iwind/TeaGo/utils/time"
@@ -12,6 +13,11 @@ type IndexAction struct {
}
func (this *IndexAction) Init() {
if this.ParamString("type") == "unread" {
this.FirstMenu("unread")
} else {
this.FirstMenu("index")
}
}
func (this *IndexAction) RunGet(params struct {
@@ -19,19 +25,34 @@ func (this *IndexAction) RunGet(params struct {
DayTo string
Keyword string
Level string
Type string
}) {
this.Data["dayFrom"] = params.DayFrom
this.Data["dayTo"] = params.DayTo
this.Data["keyword"] = params.Keyword
this.Data["level"] = params.Level
this.Data["type"] = params.Type
// 未读数量
countUnreadResp, err := this.RPC().NodeLogRPC().CountNodeLogs(this.AdminContext(), &pb.CountNodeLogsRequest{
Role: nodeconfigs.NodeRoleNode,
IsUnread: true,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["countUnreadLogs"] = countUnreadResp.Count
// 日志数量
countResp, err := this.RPC().NodeLogRPC().CountNodeLogs(this.AdminContext(), &pb.CountNodeLogsRequest{
NodeId: 0,
Role: "node",
DayFrom: params.DayFrom,
DayTo: params.DayTo,
Keyword: params.Keyword,
Level: params.Level,
NodeId: 0,
Role: nodeconfigs.NodeRoleNode,
DayFrom: params.DayFrom,
DayTo: params.DayTo,
Keyword: params.Keyword,
Level: params.Level,
IsUnread: params.Type == "unread",
})
if err != nil {
this.ErrorPage(err)
@@ -42,14 +63,15 @@ func (this *IndexAction) RunGet(params struct {
this.Data["page"] = page.AsHTML()
logsResp, err := this.RPC().NodeLogRPC().ListNodeLogs(this.AdminContext(), &pb.ListNodeLogsRequest{
NodeId: 0,
Role: "node",
DayFrom: params.DayFrom,
DayTo: params.DayTo,
Keyword: params.Keyword,
Level: params.Level,
Offset: page.Offset,
Size: page.Size,
NodeId: 0,
Role: nodeconfigs.NodeRoleNode,
DayFrom: params.DayFrom,
DayTo: params.DayTo,
Keyword: params.Keyword,
Level: params.Level,
IsUnread: params.Type == "unread",
Offset: page.Offset,
Size: page.Size,
})
if err != nil {
this.ErrorPage(err)
@@ -69,12 +91,14 @@ func (this *IndexAction) RunGet(params struct {
}
logs = append(logs, maps.Map{
"id": log.Id,
"tag": log.Tag,
"description": log.Description,
"createdTime": timeutil.FormatTime("Y-m-d H:i:s", log.CreatedAt),
"level": log.Level,
"isToday": timeutil.FormatTime("Y-m-d", log.CreatedAt) == timeutil.Format("Y-m-d"),
"count": log.Count,
"isRead": log.IsRead,
"node": maps.Map{
"id": node.Id,
"cluster": maps.Map{

View File

@@ -14,6 +14,7 @@ func init() {
Data("teaSubMenu", "log").
Prefix("/clusters/logs").
Get("", new(IndexAction)).
Post("/readLogs", new(ReadLogsAction)).
EndAll()
})
}

View File

@@ -0,0 +1,26 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package logs
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
type ReadLogsAction struct {
actionutils.ParentAction
}
func (this *ReadLogsAction) RunPost(params struct {
LogIds []int64
}) {
_, err := this.RPC().NodeLogRPC().UpdateNodeLogsRead(this.AdminContext(), &pb.UpdateNodeLogsReadRequest{
NodeLogIds: params.LogIds,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -21,6 +21,7 @@ func (this *SelectPopupAction) Init() {
func (this *SelectPopupAction) RunGet(params struct {
SelectedClusterIds string
Keyword string
PageSize int64
}) {
this.Data["keyword"] = params.Keyword
@@ -36,6 +37,9 @@ func (this *SelectPopupAction) RunGet(params struct {
var count = countResp.Count
var page = this.NewPage(count)
page.Size = 6
if params.PageSize > 0 {
page.Size = params.PageSize
}
this.Data["page"] = page.AsHTML()
clustersResp, err := this.RPC().NodeClusterRPC().ListEnabledNodeClusters(this.AdminContext(), &pb.ListEnabledNodeClustersRequest{

View File

@@ -77,7 +77,7 @@ func ValidateRecordName(name string) bool {
strings.HasSuffix(piece, "-") ||
strings.Contains(piece, "--") ||
len(piece) > 63 ||
!regexp.MustCompile(`^[a-z0-9-]+$`).MatchString(piece) {
!regexp.MustCompile(`^[_a-z0-9-]+$`).MatchString(piece) {
return false
}
}

View File

@@ -1,38 +0,0 @@
package bills
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
timeutil "github.com/iwind/TeaGo/utils/time"
"time"
)
type GenerateAction struct {
actionutils.ParentAction
}
func (this *GenerateAction) Init() {
this.Nav("", "", "generate")
}
func (this *GenerateAction) RunGet(params struct{}) {
this.Data["month"] = timeutil.Format("Ym", time.Now().AddDate(0, -1, 0))
this.Show()
}
func (this *GenerateAction) RunPost(params struct {
Month string
Must *actions.Must
}) {
defer this.CreateLogInfo("手动生成上个月(" + params.Month + ")账单")
_, err := this.RPC().UserBillRPC().GenerateAllUserBills(this.AdminContext(), &pb.GenerateAllUserBillsRequest{Month: params.Month})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -1,65 +0,0 @@
package bills
import (
"fmt"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "", "index")
}
func (this *IndexAction) RunGet(params struct {
PaidFlag int32 `default:"-1"`
UserId int64
Month string
}) {
countResp, err := this.RPC().UserBillRPC().CountAllUserBills(this.AdminContext(), &pb.CountAllUserBillsRequest{})
if err != nil {
this.ErrorPage(err)
return
}
page := this.NewPage(countResp.Count)
this.Data["page"] = page.AsHTML()
billsResp, err := this.RPC().UserBillRPC().ListUserBills(this.AdminContext(), &pb.ListUserBillsRequest{
PaidFlag: params.PaidFlag,
UserId: params.UserId,
Month: params.Month,
Offset: page.Offset,
Size: page.Size,
})
if err != nil {
this.ErrorPage(err)
return
}
billMaps := []maps.Map{}
for _, bill := range billsResp.UserBills {
var userMap maps.Map = nil
if bill.User != nil {
userMap = maps.Map{
"id": bill.User.Id,
"fullname": bill.User.Fullname,
}
}
billMaps = append(billMaps, maps.Map{
"id": bill.Id,
"isPaid": bill.IsPaid,
"month": bill.Month,
"amount": fmt.Sprintf("%.2f", bill.Amount),
"typeName": bill.TypeName,
"user": userMap,
"description": bill.Description,
})
}
this.Data["bills"] = billMaps
this.Show()
}

View File

@@ -1,22 +0,0 @@
package bills
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeFinance)).
Data("teaMenu", "finance").
// 财务管理
Prefix("/finance/bills").
Get("", new(IndexAction)).
GetPost("/generate", new(GenerateAction)).
EndAll()
})
}

View File

@@ -1,16 +0,0 @@
package finance
import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "", "")
}
func (this *IndexAction) RunGet(params struct{}) {
// TODO 暂时先跳转到账单页将来做成Dashboard
this.RedirectURL("/finance/bills")
}

View File

@@ -1,20 +0,0 @@
package finance
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeFinance)).
// 财务管理
Prefix("/finance").
Get("", new(IndexAction)).
EndAll()
})
}

View File

@@ -1,19 +0,0 @@
package logs
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeNS)).
Data("teaMenu", "ns").
Data("teaSubMenu", "accessLog").
Prefix("/ns/clusters/accessLogs").
Get("", new(IndexAction)).
EndAll()
})
}

View File

@@ -1,38 +0,0 @@
package cluster
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/ns/clusters/cluster/node"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/ns/clusters/clusterutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeNS)).
Helper(new(clusterutils.ClusterHelper)).
Data("teaMenu", "ns").
Data("teaSubMenu", "cluster").
Prefix("/ns/clusters/cluster").
Get("", new(IndexAction)).
GetPost("/delete", new(DeleteAction)).
GetPost("/createNode", new(CreateNodeAction)).
Post("/deleteNode", new(DeleteNodeAction)).
Get("/upgradeRemote", new(UpgradeRemoteAction)).
GetPost("/updateNodeSSH", new(UpdateNodeSSHAction)).
// 节点相关
Prefix("/ns/clusters/cluster/node").
Get("", new(node.IndexAction)).
Get("/logs", new(node.LogsAction)).
GetPost("/update", new(node.UpdateAction)).
GetPost("/install", new(node.InstallAction)).
Post("/status", new(node.StatusAction)).
Post("/updateInstallStatus", new(node.UpdateInstallStatusAction)).
Post("/start", new(node.StartAction)).
Post("/stop", new(node.StopAction)).
EndAll()
})
}

View File

@@ -1,21 +0,0 @@
package accessLog
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/ns/clusters/clusterutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeNS)).
Helper(new(clusterutils.ClusterHelper)).
Data("teaMenu", "ns").
Data("teaSubMenu", "cluster").
Prefix("/ns/clusters/cluster/settings/accessLog").
GetPost("", new(IndexAction)).
EndAll()
})
}

View File

@@ -1,21 +0,0 @@
package cluster
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/ns/clusters/clusterutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeNS)).
Helper(new(clusterutils.ClusterHelper)).
Data("teaMenu", "ns").
Data("teaSubMenu", "cluster").
Prefix("/ns/clusters/cluster/settings").
GetPost("", new(IndexAction)).
EndAll()
})
}

View File

@@ -1,21 +0,0 @@
package recursion
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/ns/clusters/clusterutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeNS)).
Helper(new(clusterutils.ClusterHelper)).
Data("teaMenu", "ns").
Data("teaSubMenu", "cluster").
Prefix("/ns/clusters/cluster/settings/recursion").
GetPost("", new(IndexAction)).
EndAll()
})
}

View File

@@ -1,21 +0,0 @@
package clusters
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeNS)).
Data("teaMenu", "ns").
Data("teaSubMenu", "cluster").
Prefix("/ns/clusters").
Get("", new(IndexAction)).
GetPost("/create", new(CreateAction)).
Post("/options", new(OptionsAction)).
EndAll()
})
}

View File

@@ -1,19 +0,0 @@
package logs
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeNS)).
Data("teaMenu", "ns").
Data("teaSubMenu", "log").
Prefix("/ns/clusters/logs").
Get("", new(IndexAction)).
EndAll()
})
}

View File

@@ -1,25 +0,0 @@
package clusters
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeNS)).
Data("teaMenu", "ns").
Data("teaSubMenu", "route").
Prefix("/ns/routes").
Get("", new(IndexAction)).
Get("/route", new(RouteAction)).
GetPost("/createPopup", new(CreatePopupAction)).
GetPost("/updatePopup", new(UpdatePopupAction)).
Post("/delete", new(DeleteAction)).
Post("/sort", new(SortAction)).
Post("/options", new(OptionsAction)).
EndAll()
})
}

View File

@@ -1,23 +0,0 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package accesslogs
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/ns/settings/settingutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeNS)).
Helper(new(settingutils.Helper)).
Data("teaMenu", "ns").
Data("teaSubMenu", "setting").
Prefix("/ns/settings/accesslogs").
GetPost("", new(IndexAction)).
EndAll()
})
}

View File

@@ -1,20 +0,0 @@
package test
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeNS)).
Data("teaMenu", "ns").
Data("teaSubMenu", "test").
Prefix("/ns/test").
GetPost("", new(IndexAction)).
Post("/nodeOptions", new(NodeOptionsAction)).
EndAll()
})
}

View File

@@ -1,19 +0,0 @@
package users
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeNS)).
Data("teaMenu", "ns").
Data("teaSubMenu", "domain").
Prefix("/ns/users").
Post("/options", new(OptionsAction)).
EndAll()
})
}

View File

@@ -1,26 +0,0 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package ipbox
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeServer)).
Prefix("/servers/accesslogs").
Data("teaMenu", "servers").
Data("teaSubMenu", "accesslog").
Get("", new(IndexAction)).
GetPost("/createPopup", new(CreatePopupAction)).
Get("/policy", new(PolicyAction)).
GetPost("/test", new(TestAction)).
GetPost("/update", new(UpdateAction)).
Post("/delete", new(DeleteAction)).
EndAll()
})
}

View File

@@ -19,6 +19,7 @@ func init() {
GetPost("/createPopup", new(CreatePopupAction)).
Post("/delete", new(DeleteAction)).
Get("/policy", new(PolicyAction)).
Post("/upgradeTemplate", new(UpgradeTemplateAction)).
Get("/groups", new(GroupsAction)).
Get("/group", new(GroupAction)).
Get("/log", new(LogAction)).

View File

@@ -47,6 +47,33 @@ func (this *PolicyAction) RunGet(params struct {
}
}
// 检查是否有升级
var templatePolicy = firewallconfigs.HTTPFirewallTemplate()
var upgradeItems = []string{}
if templatePolicy.Inbound != nil {
for _, group := range templatePolicy.Inbound.Groups {
if len(group.Code) == 0 {
continue
}
var oldGroup = firewallPolicy.FindRuleGroupWithCode(group.Code)
if oldGroup == nil {
upgradeItems = append(upgradeItems, group.Name)
continue
}
for _, set := range group.Sets {
if len(set.Code) == 0 {
continue
}
var oldSet = oldGroup.FindRuleSetWithCode(set.Code)
if oldSet == nil {
upgradeItems = append(upgradeItems, group.Name+" -- "+set.Name)
continue
}
}
}
}
this.Data["upgradeItems"] = upgradeItems
// 模式
if len(firewallPolicy.Mode) == 0 {
firewallPolicy.Mode = firewallconfigs.FirewallModeDefend

View File

@@ -0,0 +1,124 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package waf
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/dao"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs"
)
type UpgradeTemplateAction struct {
actionutils.ParentAction
}
func (this *UpgradeTemplateAction) RunPost(params struct {
PolicyId int64
}) {
defer this.CreateLogInfo("升级WAF %d 内置规则", params.PolicyId)
policy, err := dao.SharedHTTPFirewallPolicyDAO.FindEnabledHTTPFirewallPolicyConfig(this.AdminContext(), params.PolicyId)
if err != nil {
this.ErrorPage(err)
return
}
if policy == nil {
this.NotFound("firewallPolicy", params.PolicyId)
return
}
// 检查是否有升级
var templatePolicy = firewallconfigs.HTTPFirewallTemplate()
if templatePolicy.Inbound != nil {
for _, group := range templatePolicy.Inbound.Groups {
if len(group.Code) == 0 {
continue
}
var oldGroup = policy.FindRuleGroupWithCode(group.Code)
if oldGroup == nil {
createGroupResp, err := this.RPC().HTTPFirewallRuleGroupRPC().CreateHTTPFirewallRuleGroup(this.AdminContext(), &pb.CreateHTTPFirewallRuleGroupRequest{
IsOn: true,
Name: group.Name,
Code: group.Code,
Description: group.Description,
})
if err != nil {
this.ErrorPage(err)
return
}
var groupId = createGroupResp.FirewallRuleGroupId
policy.Inbound.GroupRefs = append(policy.Inbound.GroupRefs, &firewallconfigs.HTTPFirewallRuleGroupRef{
IsOn: true,
GroupId: groupId,
})
for _, set := range group.Sets {
setJSON, err := json.Marshal(set)
if err != nil {
this.ErrorPage(err)
return
}
_, err = this.RPC().HTTPFirewallRuleGroupRPC().AddHTTPFirewallRuleGroupSet(this.AdminContext(), &pb.AddHTTPFirewallRuleGroupSetRequest{
FirewallRuleGroupId: groupId,
FirewallRuleSetConfigJSON: setJSON,
})
if err != nil {
this.ErrorPage(err)
return
}
}
continue
}
for _, set := range group.Sets {
if len(set.Code) == 0 {
continue
}
var oldSet = oldGroup.FindRuleSetWithCode(set.Code)
if oldSet == nil {
setJSON, err := json.Marshal(set)
if err != nil {
this.ErrorPage(err)
return
}
_, err = this.RPC().HTTPFirewallRuleGroupRPC().AddHTTPFirewallRuleGroupSet(this.AdminContext(), &pb.AddHTTPFirewallRuleGroupSetRequest{
FirewallRuleGroupId: oldGroup.Id,
FirewallRuleSetConfigJSON: setJSON,
})
if err != nil {
this.ErrorPage(err)
return
}
continue
}
}
}
}
// 保存inbound
inboundJSON, err := policy.InboundJSON()
if err != nil {
this.ErrorPage(err)
return
}
outboundJSON, err := policy.OutboundJSON()
if err != nil {
this.ErrorPage(err)
return
}
_, err = this.RPC().HTTPFirewallPolicyRPC().UpdateHTTPFirewallPolicyGroups(this.AdminContext(), &pb.UpdateHTTPFirewallPolicyGroupsRequest{
HttpFirewallPolicyId: params.PolicyId,
InboundJSON: inboundJSON,
OutboundJSON: outboundJSON,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/dao"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/logs"
@@ -71,8 +72,11 @@ func (this *CreateAction) RunPost(params struct {
CertIdsJSON []byte
Origins string
AccessLogIsOn bool
WebsocketIsOn bool
AccessLogIsOn bool
WebsocketIsOn bool
CacheIsOn bool
WafIsOn bool
RemoteAddrIsOn bool
WebRoot string
@@ -483,6 +487,75 @@ func (this *CreateAction) RunPost(params struct {
}
}
}
// cache
if params.CacheIsOn {
var cacheConfig = &serverconfigs.HTTPCacheConfig{
IsPrior: false,
IsOn: true,
AddStatusHeader: true,
PurgeIsOn: false,
PurgeKey: "",
CacheRefs: nil,
}
cacheConfigJSON, err := json.Marshal(cacheConfig)
if err != nil {
this.ErrorPage(err)
return
}
_, err = this.RPC().HTTPWebRPC().UpdateHTTPWebCache(this.AdminContext(), &pb.UpdateHTTPWebCacheRequest{
WebId: webConfig.Id,
CacheJSON: cacheConfigJSON,
})
if err != nil {
this.ErrorPage(err)
return
}
}
// waf
if params.WafIsOn {
var firewallRef = &firewallconfigs.HTTPFirewallRef{
IsPrior: false,
IsOn: true,
FirewallPolicyId: 0,
}
firewallRefJSON, err := json.Marshal(firewallRef)
if err != nil {
this.ErrorPage(err)
return
}
_, err = this.RPC().HTTPWebRPC().UpdateHTTPWebFirewall(this.AdminContext(), &pb.UpdateHTTPWebFirewallRequest{
WebId: webConfig.Id,
FirewallJSON: firewallRefJSON,
})
if err != nil {
this.ErrorPage(err)
return
}
}
// remoteAddr
var remoteAddrConfig = &serverconfigs.HTTPRemoteAddrConfig{
IsOn: true,
Value: "${rawRemoteAddr}",
}
if params.RemoteAddrIsOn {
remoteAddrConfig.Value = "${remoteAddr}"
}
remoteAddrConfigJSON, err := json.Marshal(remoteAddrConfig)
if err != nil {
this.ErrorPage(err)
return
}
_, err = this.RPC().HTTPWebRPC().UpdateHTTPWebRemoteAddr(this.AdminContext(), &pb.UpdateHTTPWebRemoteAddrRequest{
WebId: webConfig.Id,
RemoteAddrJSON: remoteAddrConfigJSON,
})
if err != nil {
this.ErrorPage(err)
return
}
}
}

View File

@@ -87,15 +87,26 @@ func (this *SettingAction) RunPost(params struct {
return
}
// PROXY Protocol
var proxyProtocolJSON = []byte{}
if reverseProxyConfig.ProxyProtocol != nil {
proxyProtocolJSON, err = json.Marshal(reverseProxyConfig.ProxyProtocol)
if err != nil {
this.ErrorPage(err)
return
}
}
// 设置反向代理相关信息
_, err = this.RPC().ReverseProxyRPC().UpdateReverseProxy(this.AdminContext(), &pb.UpdateReverseProxyRequest{
ReverseProxyId: reverseProxyConfig.Id,
RequestHostType: types.Int32(reverseProxyConfig.RequestHostType),
RequestHost: reverseProxyConfig.RequestHost,
RequestURI: reverseProxyConfig.RequestURI,
StripPrefix: reverseProxyConfig.StripPrefix,
AutoFlush: reverseProxyConfig.AutoFlush,
AddHeaders: reverseProxyConfig.AddHeaders,
ReverseProxyId: reverseProxyConfig.Id,
RequestHostType: types.Int32(reverseProxyConfig.RequestHostType),
RequestHost: reverseProxyConfig.RequestHost,
RequestURI: reverseProxyConfig.RequestURI,
StripPrefix: reverseProxyConfig.StripPrefix,
AutoFlush: reverseProxyConfig.AutoFlush,
AddHeaders: reverseProxyConfig.AddHeaders,
ProxyProtocolJSON: proxyProtocolJSON,
})
this.Success()

View File

@@ -87,15 +87,26 @@ func (this *SettingAction) RunPost(params struct {
return
}
// PROXY Protocol
var proxyProtocolJSON = []byte{}
if reverseProxyConfig.ProxyProtocol != nil {
proxyProtocolJSON, err = json.Marshal(reverseProxyConfig.ProxyProtocol)
if err != nil {
this.ErrorPage(err)
return
}
}
// 设置反向代理相关信息
_, err = this.RPC().ReverseProxyRPC().UpdateReverseProxy(this.AdminContext(), &pb.UpdateReverseProxyRequest{
ReverseProxyId: reverseProxyConfig.Id,
RequestHostType: types.Int32(reverseProxyConfig.RequestHostType),
RequestHost: reverseProxyConfig.RequestHost,
RequestURI: reverseProxyConfig.RequestURI,
StripPrefix: reverseProxyConfig.StripPrefix,
AutoFlush: reverseProxyConfig.AutoFlush,
AddHeaders: reverseProxyConfig.AddHeaders,
ReverseProxyId: reverseProxyConfig.Id,
RequestHostType: types.Int32(reverseProxyConfig.RequestHostType),
RequestHost: reverseProxyConfig.RequestHost,
RequestURI: reverseProxyConfig.RequestURI,
StripPrefix: reverseProxyConfig.StripPrefix,
AutoFlush: reverseProxyConfig.AutoFlush,
AddHeaders: reverseProxyConfig.AddHeaders,
ProxyProtocolJSON: proxyProtocolJSON,
})
this.Success()

View File

@@ -19,8 +19,11 @@ func (this *ItemsAction) Init() {
}
func (this *ItemsAction) RunGet(params struct {
ListId int64
ListId int64
Keyword string
}) {
this.Data["keyword"] = params.Keyword
err := InitIPList(this.Parent(), params.ListId)
if err != nil {
this.ErrorPage(err)
@@ -29,7 +32,10 @@ func (this *ItemsAction) RunGet(params struct {
// 数量
var listId = params.ListId
countResp, err := this.RPC().IPItemRPC().CountIPItemsWithListId(this.AdminContext(), &pb.CountIPItemsWithListIdRequest{IpListId: listId})
countResp, err := this.RPC().IPItemRPC().CountIPItemsWithListId(this.AdminContext(), &pb.CountIPItemsWithListIdRequest{
IpListId: listId,
Keyword: params.Keyword,
})
if err != nil {
this.ErrorPage(err)
return
@@ -41,6 +47,7 @@ func (this *ItemsAction) RunGet(params struct {
// 列表
itemsResp, err := this.RPC().IPItemRPC().ListIPItemsWithListId(this.AdminContext(), &pb.ListIPItemsWithListIdRequest{
IpListId: listId,
Keyword: params.Keyword,
Offset: page.Offset,
Size: page.Size,
})

View File

@@ -3,6 +3,7 @@ package dns
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
)
type IndexAction struct {
@@ -28,6 +29,28 @@ func (this *IndexAction) RunGet(params struct {
} else {
this.Data["dnsDomain"] = ""
}
this.Data["supportCNAME"] = dnsInfoResp.SupportCNAME
this.Show()
}
func (this *IndexAction) RunPost(params struct {
ServerId int64
SupportCNAME bool
Must *actions.Must
CSRF *actionutils.CSRF
}) {
defer this.CreateLogInfo("修改服务 %d 的DNS设置", params.ServerId)
_, err := this.RPC().ServerRPC().UpdateServerDNS(this.AdminContext(), &pb.UpdateServerDNSRequest{
ServerId: params.ServerId,
SupportCNAME: params.SupportCNAME,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -14,6 +14,7 @@ func init() {
Helper(serverutils.NewServerHelper()).
Prefix("/servers/server/settings/dns").
GetPost("", new(IndexAction)).
Post("/regenerateCNAME", new(RegenerateCNAMEAction)).
EndAll()
})
}

View File

@@ -0,0 +1,26 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package dns
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
type RegenerateCNAMEAction struct {
actionutils.ParentAction
}
func (this *RegenerateCNAMEAction) RunPost(params struct {
ServerId int64
}) {
defer this.CreateLogInfo("重新生成服务 %d 的CNAME", params.ServerId)
_, err := this.RPC().ServerRPC().RegenerateServerCNAME(this.AdminContext(), &pb.RegenerateServerCNAMERequest{ServerId: params.ServerId})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -78,15 +78,26 @@ func (this *SettingAction) RunPost(params struct {
return
}
// PROXY Protocol
var proxyProtocolJSON = []byte{}
if reverseProxyConfig.ProxyProtocol != nil {
proxyProtocolJSON, err = json.Marshal(reverseProxyConfig.ProxyProtocol)
if err != nil {
this.ErrorPage(err)
return
}
}
// 设置反向代理相关信息
_, err = this.RPC().ReverseProxyRPC().UpdateReverseProxy(this.AdminContext(), &pb.UpdateReverseProxyRequest{
ReverseProxyId: reverseProxyConfig.Id,
RequestHostType: types.Int32(reverseProxyConfig.RequestHostType),
RequestHost: reverseProxyConfig.RequestHost,
RequestURI: reverseProxyConfig.RequestURI,
StripPrefix: reverseProxyConfig.StripPrefix,
AutoFlush: reverseProxyConfig.AutoFlush,
AddHeaders: reverseProxyConfig.AddHeaders,
ReverseProxyId: reverseProxyConfig.Id,
RequestHostType: types.Int32(reverseProxyConfig.RequestHostType),
RequestHost: reverseProxyConfig.RequestHost,
RequestURI: reverseProxyConfig.RequestURI,
StripPrefix: reverseProxyConfig.StripPrefix,
AutoFlush: reverseProxyConfig.AutoFlush,
AddHeaders: reverseProxyConfig.AddHeaders,
ProxyProtocolJSON: proxyProtocolJSON,
})
this.Success()

View File

@@ -106,20 +106,31 @@ func (this *SettingAction) RunPost(params struct {
return
}
// PROXY Protocol
var proxyProtocolJSON = []byte{}
if reverseProxyConfig.ProxyProtocol != nil {
proxyProtocolJSON, err = json.Marshal(reverseProxyConfig.ProxyProtocol)
if err != nil {
this.ErrorPage(err)
return
}
}
// 设置反向代理相关信息
_, err = this.RPC().ReverseProxyRPC().UpdateReverseProxy(this.AdminContext(), &pb.UpdateReverseProxyRequest{
ReverseProxyId: reverseProxyConfig.Id,
RequestHostType: types.Int32(reverseProxyConfig.RequestHostType),
RequestHost: reverseProxyConfig.RequestHost,
RequestURI: reverseProxyConfig.RequestURI,
StripPrefix: reverseProxyConfig.StripPrefix,
AutoFlush: reverseProxyConfig.AutoFlush,
AddHeaders: reverseProxyConfig.AddHeaders,
ConnTimeoutJSON: connTimeoutJSON,
ReadTimeoutJSON: readTimeoutJSON,
IdleTimeoutJSON: idleTimeoutJSON,
MaxConns: types.Int32(reverseProxyConfig.MaxConns),
MaxIdleConns: types.Int32(reverseProxyConfig.MaxIdleConns),
ReverseProxyId: reverseProxyConfig.Id,
RequestHostType: types.Int32(reverseProxyConfig.RequestHostType),
RequestHost: reverseProxyConfig.RequestHost,
RequestURI: reverseProxyConfig.RequestURI,
StripPrefix: reverseProxyConfig.StripPrefix,
AutoFlush: reverseProxyConfig.AutoFlush,
AddHeaders: reverseProxyConfig.AddHeaders,
ConnTimeoutJSON: connTimeoutJSON,
ReadTimeoutJSON: readTimeoutJSON,
IdleTimeoutJSON: idleTimeoutJSON,
MaxConns: types.Int32(reverseProxyConfig.MaxConns),
MaxIdleConns: types.Int32(reverseProxyConfig.MaxIdleConns),
ProxyProtocolJSON: proxyProtocolJSON,
})
if err != nil {
this.ErrorPage(err)

View File

@@ -348,6 +348,15 @@ func (this *ServerHelper) createSettingsMenu(secondMenuItem string, serverIdStri
"isActive": secondMenuItem == "remoteAddr",
"isOn": serverConfig.Web != nil && serverConfig.Web.RemoteAddr != nil && serverConfig.Web.RemoteAddr.IsOn,
})
if teaconst.IsPlus {
menuItems = append(menuItems, maps.Map{
"name": "带宽限制",
"url": "/servers/server/settings/bandwidth?serverId=" + serverIdString,
"isActive": secondMenuItem == "bandwidth",
"isOn": serverConfig.BandwidthLimit != nil && serverConfig.BandwidthLimit.IsOn,
})
}
} else if serverConfig.IsTCPFamily() {
menuItems = append(menuItems, maps.Map{
"name": "TCP",

View File

@@ -2,6 +2,7 @@ package settingutils
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/actions"
)
@@ -31,7 +32,9 @@ func (this *Helper) BeforeAction(actionPtr actions.ActionWrapper) (goNext bool)
if configloaders.AllowModule(adminId, configloaders.AdminModuleCodeSetting) {
tabbar.Add("Web服务", "", "/settings/server", "", this.tab == "server")
tabbar.Add("管理界面设置", "", "/settings/ui", "", this.tab == "ui")
tabbar.Add("用户界面设置", "", "/settings/user-ui", "", this.tab == "userUI")
if teaconst.IsPlus {
tabbar.Add("用户界面设置", "", "/settings/user-ui", "", this.tab == "userUI")
}
tabbar.Add("安全设置", "", "/settings/security", "", this.tab == "security")
tabbar.Add("IP库", "", "/settings/ip-library", "", this.tab == "ipLibrary")
}

View File

@@ -4,7 +4,11 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeAdmin/internal/setup"
"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"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps"
"net"
"net/http"
@@ -130,7 +134,7 @@ func (this *userMustAuth) BeforeAction(actionPtr actions.ActionWrapper, paramNam
if !action.Data.Has("teaMenu") {
action.Data["teaMenu"] = ""
}
action.Data["teaModules"] = this.modules(adminId)
action.Data["teaModules"] = this.modules(actionPtr, adminId)
action.Data["teaSubMenus"] = []map[string]interface{}{}
action.Data["teaTabbar"] = []map[string]interface{}{}
if len(config.Version) == 0 {
@@ -165,7 +169,36 @@ func (this *userMustAuth) BeforeAction(actionPtr actions.ActionWrapper, paramNam
}
// 菜单配置
func (this *userMustAuth) modules(adminId int64) []maps.Map {
func (this *userMustAuth) modules(actionPtr actions.ActionWrapper, adminId int64) []maps.Map {
var countUnreadNodeLogs int64 = 0
var nodeLogsType = ""
// 父级动作
parentAction, ok := actionPtr.(actionutils.ActionInterface)
if ok {
var action = actionPtr.Object()
// 未读日志数
if action.Data.GetString("teaMenu") == "clusters" {
countNodeLogsResp, err := parentAction.RPC().NodeLogRPC().CountNodeLogs(parentAction.AdminContext(), &pb.CountNodeLogsRequest{
Role: nodeconfigs.NodeRoleNode,
IsUnread: true,
})
if err != nil {
logs.Error(err)
} else {
var countNodeLogs = countNodeLogsResp.Count
if countNodeLogs > 0 {
countUnreadNodeLogs = countNodeLogs
if countUnreadNodeLogs >= 1000 {
countUnreadNodeLogs = 999
}
nodeLogsType = "unread"
}
}
}
}
allMaps := []maps.Map{
{
"code": "dashboard",
@@ -236,9 +269,10 @@ func (this *userMustAuth) modules(adminId int64) []maps.Map {
"icon": "cloud",
"subItems": []maps.Map{
{
"name": "运行日志",
"url": "/clusters/logs",
"code": "log",
"name": "运行日志",
"url": "/clusters/logs?type=" + nodeLogsType,
"code": "log",
"badge": countUnreadNodeLogs,
},
{
"name": "IP地址",
@@ -340,6 +374,21 @@ func (this *userMustAuth) modules(adminId int64) []maps.Map {
"icon": "yen sign",
"isOn": teaconst.IsPlus,
},
/**{
"code": "plans",
"module": configloaders.AdminModuleCodePlan,
"name": "套餐管理",
"icon": "puzzle piece",
"isOn": teaconst.IsPlus,
"subItems": []maps.Map{
{
"name": "已购套餐",
"url": "/plans/userPlans",
"code": "userPlans",
"isOn": teaconst.IsPlus,
},
},
},**/
{
"code": "admins",
"module": configloaders.AdminModuleCodeAdmin,

View File

@@ -27,8 +27,6 @@ import (
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/db"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/dns"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/dns/tasks"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/finance"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/finance/bills"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/index"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/log"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/logout"

View File

@@ -1,47 +0,0 @@
{
"name": "codemirror",
"version": "5.41.0",
"main": "lib/codemirror.js",
"style": "lib/codemirror.css",
"author": {
"name": "Marijn Haverbeke",
"email": "marijnh@gmail.com",
"url": "http://marijnhaverbeke.nl"
},
"description": "Full-featured in-browser code editor",
"license": "MIT",
"directories": {
"lib": "./lib"
},
"scripts": {
"build": "rollup -c",
"watch": "rollup -w -c",
"prepare": "npm run-script build",
"test": "node ./test/run.js",
"lint": "bin/lint"
},
"devDependencies": {
"blint": "^1",
"node-static": "0.7.11",
"phantomjs-prebuilt": "^2.1.12",
"rollup": "^0.66.2",
"rollup-plugin-buble": "^0.19.2",
"rollup-watch": "^4.3.1"
},
"bugs": "http://github.com/codemirror/CodeMirror/issues",
"keywords": [
"JavaScript",
"CodeMirror",
"Editor"
],
"homepage": "https://codemirror.net",
"repository": {
"type": "git",
"url": "https://github.com/codemirror/CodeMirror.git"
},
"jspm": {
"directories": {},
"dependencies": {},
"devDependencies": {}
}
}

View File

@@ -1,20 +0,0 @@
import buble from 'rollup-plugin-buble';
export default {
input: "src/codemirror.js",
output: {
banner: `// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
// This is CodeMirror (https://codemirror.net), a code editor
// implemented in JavaScript on top of the browser's DOM.
//
// You can find some technical background for some of the code below
// at http://marijnhaverbeke.nl/blog/#cm-internals .
`,
format: "umd",
file: "lib/codemirror.js",
name: "CodeMirror"
},
plugins: [ buble({namedFunctionExpressions: false}) ]
};

View File

@@ -0,0 +1,34 @@
Vue.component("datepicker", {
props: ["v-name", "v-value", "v-bottom-left"],
mounted: function () {
let that = this
teaweb.datepicker(this.$refs.dayInput, function (v) {
that.day = v
that.change()
}, !!this.vBottomLeft)
},
data: function () {
let name = this.vName
if (name == null) {
name = "day"
}
let day = this.vValue
if (day == null) {
day = ""
}
return {
name: name,
day: day
}
},
methods: {
change: function () {
this.$emit("change", this.day)
}
},
template: `<div style="display: inline-block">
<input type="text" :name="name" v-model="day" placeholder="YYYY-MM-DD" style="width:8.6em" maxlength="10" @input="change" ref="dayInput" autocomplete="off"/>
</div>`
})

View File

@@ -18,7 +18,9 @@ Vue.component("health-check-config-box", {
tryDelay: {count: 100, unit: "ms"},
autoDown: true,
countUp: 1,
countDown: 3
countDown: 3,
userAgent: "",
onlyBasicRequest: false
}
let that = this
setTimeout(function () {
@@ -72,7 +74,8 @@ Vue.component("health-check-config-box", {
urlProtocol: urlProtocol,
urlHost: urlHost,
urlPort: urlPort,
urlRequestURI: urlRequestURI
urlRequestURI: urlRequestURI,
urlIsEditing: healthCheckConfig.url.length == 0
}
},
watch: {
@@ -142,6 +145,9 @@ Vue.component("health-check-config-box", {
return status
}
})
},
editURL: function () {
this.urlIsEditing = !this.urlIsEditing
}
},
template: `<div>
@@ -162,41 +168,41 @@ Vue.component("health-check-config-box", {
<tr>
<td>URL *</td>
<td>
<table class="ui table">
<tr>
<td class="title">协议</td>
<td>
<select class="ui dropdown auto-width" v-model="urlProtocol">
<option value="http">http://</option>
<option value="https">https://</option>
</select>
</td>
</tr>
<tr>
<td>域名</td>
<td>
<input type="text" v-model="urlHost"/>
<p class="comment">在此集群上可以访问到的一个域名。</p>
</td>
</tr>
<tr>
<td>端口</td>
<td>
<input type="text" maxlength="5" style="width:5.4em" placeholder="端口" v-model="urlPort"/>
</td>
</tr>
<tr>
<td>RequestURI</td>
<td><input type="text" v-model="urlRequestURI" placeholder="/" style="width:20em"/></td>
</tr>
</table>
<div class="ui divider"></div>
<p class="comment" v-if="healthCheck.url.length > 0">拼接后的URL<code-label>{{healthCheck.url}}</code-label>,其中\${host}指的是域名。</p>
<div v-if="healthCheck.url.length > 0" style="margin-bottom: 1em"><code-label>{{healthCheck.url}}</code-label> &nbsp; <a href="" @click.prevent="editURL"><span class="small">修改 <i class="icon angle" :class="{down: !urlIsEditing, up: urlIsEditing}"></i></span></a> </div>
<div v-show="urlIsEditing">
<table class="ui table">
<tr>
<td class="title">协议</td>
<td>
<select class="ui dropdown auto-width" v-model="urlProtocol">
<option value="http">http://</option>
<option value="https">https://</option>
</select>
</td>
</tr>
<tr>
<td>域名</td>
<td>
<input type="text" v-model="urlHost"/>
<p class="comment">在此集群上可以访问到的一个域名。</p>
</td>
</tr>
<tr>
<td>端口</td>
<td>
<input type="text" maxlength="5" style="width:5.4em" placeholder="端口" v-model="urlPort"/>
</td>
</tr>
<tr>
<td>RequestURI</td>
<td><input type="text" v-model="urlRequestURI" placeholder="/" style="width:20em"/></td>
</tr>
</table>
<div class="ui divider"></div>
<p class="comment" v-if="healthCheck.url.length > 0">拼接后的URL<code-label>{{healthCheck.url}}</code-label>,其中\${host}指的是域名。</p>
</div>
</td>
</tr>
<tr>
<td></td>
</tr>
<tr>
<td>检测时间间隔</td>
<td>
@@ -258,6 +264,20 @@ Vue.component("health-check-config-box", {
<time-duration-box :v-value="healthCheck.tryDelay"></time-duration-box>
</td>
</tr>
<tr>
<td>终端信息<em>User-Agent</em></td>
<td>
<input type="text" v-model="healthCheck.userAgent" maxlength="200"/>
<p class="comment">发送到服务器的User-Agent值不填写表示使用默认值。</p>
</td>
</tr>
<tr>
<td>只基础请求</td>
<td>
<checkbox v-model="healthCheck.onlyBasicRequest"></checkbox>
<p class="comment">只做基础的请求不处理反向代理不检查源站、WAF等。</p>
</td>
</tr>
</tbody>
</table>
<div class="margin"></div>

View File

@@ -0,0 +1,37 @@
// 节点角色名称
Vue.component("node-role-name", {
props: ["v-role"],
data: function () {
let roleName = ""
switch (this.vRole) {
case "node":
roleName = "边缘节点"
break
case "monitor":
roleName = "监控节点"
break
case "api":
roleName = "API节点"
break
case "user":
roleName = "用户平台"
break
case "admin":
roleName = "管理平台"
break
case "database":
roleName = "数据库节点"
break
case "dns":
roleName = "DNS节点"
break
case "report":
roleName = "区域监控终端"
break
}
return {
roleName: roleName
}
},
template: `<span>{{roleName}}</span>`
})

View File

@@ -60,6 +60,8 @@ Vue.component("size-capacity-box", {
<option value="kb">KB</option>
<option value="mb">MB</option>
<option value="gb">GB</option>
<option value="tb">TB</option>
<option value="pb">PB</option>
</select>
</div>
</div>`

View File

@@ -80,7 +80,7 @@ Vue.component("dns-route-selector", {
return
}
this.searchingRoutes = this.vAllRoutes.filter(function (route) {
return teaweb.match(route.name, keyword)
return teaweb.match(route.name, keyword) || teaweb.match(route.domainName, keyword)
})
if (this.searchingRoutes.length > 0) {
this.routeCode = this.searchingRoutes[0].code + "@" + this.searchingRoutes[0].domainId

View File

@@ -1,8 +1,9 @@
Vue.component("ip-list-table", {
props: ["v-items"],
props: ["v-items", "v-keyword"],
data: function () {
return {
items: this.vItems
items: this.vItems,
keyword: (this.vKeyword != null) ? this.vKeyword : ""
}
},
methods: {
@@ -33,7 +34,7 @@ Vue.component("ip-list-table", {
</thead>
<tr v-for="item in items">
<td>
<span v-if="item.type != 'all'">{{item.ipFrom}}<span v-if="item.ipTo.length > 0"> - {{item.ipTo}}</span></span>
<span v-if="item.type != 'all'"><keyword :v-word="keyword">{{item.ipFrom}}</keyword><span v-if="item.ipTo.length > 0"> - <keyword :v-word="keyword">{{item.ipTo}}</keyword></span></span>
<span v-else class="disabled">*</span>
</td>
<td>

View File

@@ -0,0 +1,166 @@
// 套餐价格配置
Vue.component("plan-price-config-box", {
props: ["v-price-type", "v-monthly-price", "v-seasonally-price", "v-yearly-price", "v-bandwidth-price"],
data: function () {
let priceType = this.vPriceType
if (priceType == null) {
priceType = "period"
}
let monthlyPriceNumber = 0
let monthlyPrice = this.vMonthlyPrice
if (monthlyPrice == null || monthlyPrice <= 0) {
monthlyPrice = ""
} else {
monthlyPrice = monthlyPrice.toString()
monthlyPriceNumber = parseFloat(monthlyPrice)
if (isNaN(monthlyPriceNumber)) {
monthlyPriceNumber = 0
}
}
let seasonallyPriceNumber = 0
let seasonallyPrice = this.vSeasonallyPrice
if (seasonallyPrice == null || seasonallyPrice <= 0) {
seasonallyPrice = ""
} else {
seasonallyPrice = seasonallyPrice.toString()
seasonallyPriceNumber = parseFloat(seasonallyPrice)
if (isNaN(seasonallyPriceNumber)) {
seasonallyPriceNumber = 0
}
}
let yearlyPriceNumber = 0
let yearlyPrice = this.vYearlyPrice
if (yearlyPrice == null || yearlyPrice <= 0) {
yearlyPrice = ""
} else {
yearlyPrice = yearlyPrice.toString()
yearlyPriceNumber = parseFloat(yearlyPrice)
if (isNaN(yearlyPriceNumber)) {
yearlyPriceNumber = 0
}
}
let bandwidthPrice = this.vBandwidthPrice
let bandwidthPriceBaseNumber = 0
if (bandwidthPrice != null) {
bandwidthPriceBaseNumber = bandwidthPrice.base
} else {
bandwidthPrice = {
base: 0
}
}
let bandwidthPriceBase = ""
if (bandwidthPriceBaseNumber > 0) {
bandwidthPriceBase = bandwidthPriceBaseNumber.toString()
}
return {
priceType: priceType,
monthlyPrice: monthlyPrice,
seasonallyPrice: seasonallyPrice,
yearlyPrice: yearlyPrice,
monthlyPriceNumber: monthlyPriceNumber,
seasonallyPriceNumber: seasonallyPriceNumber,
yearlyPriceNumber: yearlyPriceNumber,
bandwidthPriceBase: bandwidthPriceBase,
bandwidthPrice: bandwidthPrice
}
},
watch: {
monthlyPrice: function (v) {
let price = parseFloat(v)
if (isNaN(price)) {
price = 0
}
this.monthlyPriceNumber = price
},
seasonallyPrice: function (v) {
let price = parseFloat(v)
if (isNaN(price)) {
price = 0
}
this.seasonallyPriceNumber = price
},
yearlyPrice: function (v) {
let price = parseFloat(v)
if (isNaN(price)) {
price = 0
}
this.yearlyPriceNumber = price
},
bandwidthPriceBase: function (v) {
let price = parseFloat(v)
if (isNaN(price)) {
price = 0
}
this.bandwidthPrice.base = price
}
},
template: `<div>
<input type="hidden" name="priceType" :value="priceType"/>
<input type="hidden" name="monthlyPrice" :value="monthlyPriceNumber"/>
<input type="hidden" name="seasonallyPrice" :value="seasonallyPriceNumber"/>
<input type="hidden" name="yearlyPrice" :value="yearlyPriceNumber"/>
<input type="hidden" name="bandwidthPriceJSON" :value="JSON.stringify(bandwidthPrice)"/>
<div>
<radio :v-value="'period'" :value="priceType" v-model="priceType">&nbsp;按时间周期</radio> &nbsp; &nbsp;
<radio :v-value="'bandwidth'" :value="priceType" v-model="priceType">&nbsp;按带宽用量</radio>
</div>
<!-- 按时间周期 -->
<div v-show="priceType == 'period'">
<div class="ui divider"></div>
<table class="ui table">
<tr>
<td class="title">月度价格</td>
<td>
<div class="ui input right labeled">
<input type="text" style="width: 7em" maxlength="10" v-model="monthlyPrice"/>
<span class="ui label">元</span>
</div>
</td>
</tr>
<tr>
<td class="title">季度价格</td>
<td>
<div class="ui input right labeled">
<input type="text" style="width: 7em" maxlength="10" v-model="seasonallyPrice"/>
<span class="ui label">元</span>
</div>
</td>
</tr>
<tr>
<td class="title">年度价格</td>
<td>
<div class="ui input right labeled">
<input type="text" style="width: 7em" maxlength="10" v-model="yearlyPrice"/>
<span class="ui label">元</span>
</div>
</td>
</tr>
</table>
</div>
<!-- 按带宽 -->
<div v-show="priceType =='bandwidth'">
<div class="ui divider"></div>
<table class="ui table">
<tr>
<td class="title">基础带宽费用</td>
<td>
<div class="ui input right labeled">
<input type="text" v-model="bandwidthPriceBase" maxlength="10" style="width: 7em"/>
<span class="ui label">元/GB</span>
</div>
</td>
</tr>
</table>
</div>
</div>`
})

View File

@@ -0,0 +1,18 @@
Vue.component("plan-price-view", {
props: ["v-plan"],
data: function () {
return {
plan: this.vPlan
}
},
template: `<div>
<span v-if="plan.priceType == 'period'">
<span v-if="plan.monthlyPrice > 0">月度:¥{{plan.monthlyPrice}}元<br/></span>
<span v-if="plan.seasonallyPrice > 0">季度:¥{{plan.seasonallyPrice}}元<br/></span>
<span v-if="plan.yearlyPrice > 0">年度:¥{{plan.yearlyPrice}}元</span>
</span>
<span v-if="plan.priceType == 'bandwidth'">
基础价格:¥{{plan.bandwidthPrice.base}}元/GB
</span>
</div>`
})

View File

@@ -0,0 +1,28 @@
Vue.component("plan-user-selector", {
mounted: function () {
let that = this
Tea.action("/plans/users/options")
.post()
.success(function (resp) {
that.users = resp.data.users
})
},
props: ["v-user-id"],
data: function () {
let userId = this.vUserId
if (userId == null) {
userId = 0
}
return {
users: [],
userId: userId
}
},
template: `<div>
<select class="ui dropdown auto-width" name="userId" v-model="userId">
<option value="0">[选择用户]</option>
<option v-for="user in users" :value="user.id">{{user.fullname}} ({{user.username}})</option>
</select>
</div>`
})

View File

@@ -0,0 +1,102 @@
Vue.component("bandwidth-limit-config-box", {
props: ["v-bandwidth-limit"],
data: function () {
let config = this.vBandwidthLimit
if (config == null) {
config = {
isOn: false,
dailySize: {
count: -1,
unit: "gb"
},
monthlySize: {
count: -1,
unit: "gb"
},
totalSize: {
count: -1,
unit: "gb"
},
noticePageBody: ""
}
}
if (config.dailySize == null) {
config.dailySize = {
count: -1,
unit: "gb"
}
}
if (config.monthlySize == null) {
config.monthlySize = {
count: -1,
unit: "gb"
}
}
if (config.totalSize == null) {
config.totalSize = {
count: -1,
unit: "gb"
}
}
return {
config: config
}
},
methods: {
showBodyTemplate: function () {
this.config.noticePageBody = `<!DOCTYPE html>
<html>
<head>
<title>Bandwidth Limit Exceeded Warning</title>
<body>
The site bandwidth has exceeded the limit. Please contact with the site administrator.
</body>
</html>`
}
},
template: `<div>
<input type="hidden" name="bandwidthLimitJSON" :value="JSON.stringify(config)"/>
<table class="ui table selectable definition">
<tbody>
<tr>
<td class="title">是否启用</td>
<td>
<checkbox v-model="config.isOn"></checkbox>
<p class="comment">注意由于带宽统计是每5分钟统计一次所以超出带宽限制后对用户的提醒也会有所延迟。</p>
</td>
</tr>
</tbody>
<tbody v-show="config.isOn">
<tr>
<td>日带宽限制</td>
<td>
<size-capacity-box :v-value="config.dailySize"></size-capacity-box>
</td>
</tr>
<tr>
<td>月带宽限制</td>
<td>
<size-capacity-box :v-value="config.monthlySize"></size-capacity-box>
</td>
</tr>
<!--<tr>
<td>总体限制</td>
<td>
<size-capacity-box :v-value="config.totalSize"></size-capacity-box>
<p class="comment"></p>
</td>
</tr>-->
<tr>
<td>网页提示内容</td>
<td>
<textarea v-model="config.noticePageBody"></textarea>
<p class="comment"><a href="" @click.prevent="showBodyTemplate">[使用模板]</a>。当达到带宽限制时网页显示的HTML内容不填写则显示默认的提示内容。</p>
</td>
</tr>
</tbody>
</table>
<div class="margin"></div>
</div>`
})

View File

@@ -0,0 +1,16 @@
// 显示带宽限制说明
Vue.component("bandwidth-limit-view", {
props: ["v-bandwidth-limit"],
data: function () {
return {
config: this.vBandwidthLimit
}
},
template: `<div>
<div v-if="config.isOn">
<span v-if="config.dailySize != null && config.dailySize.count > 0">日带宽限制:{{config.dailySize.count}}{{config.dailySize.unit.toUpperCase()}}<br/></span>
<span v-if="config.monthlySize != null && config.monthlySize.count > 0">月带宽限制:{{config.monthlySize.count}}{{config.monthlySize.unit.toUpperCase()}}<br/></span>
</div>
<span v-else class="disabled">没有限制。</span>
</div>`
})

View File

@@ -1,5 +1,5 @@
Vue.component("http-access-log-box", {
props: ["v-access-log", "v-keyword"],
props: ["v-access-log", "v-keyword", "v-show-server-link"],
data: function () {
let accessLog = this.vAccessLog
if (accessLog.header != null && accessLog.header.Upgrade != null && accessLog.header.Upgrade.values != null && accessLog.header.Upgrade.values.$contains("websocket")) {
@@ -49,7 +49,9 @@ Vue.component("http-access-log-box", {
}
},
template: `<div style="word-break: break-all" :style="{'color': (accessLog.status >= 400) ? '#dc143c' : ''}" ref="box">
<a v-if="accessLog.node != null && accessLog.node.nodeCluster != null" :href="'/clusters/cluster/node?nodeId=' + accessLog.node.id + '&clusterId=' + accessLog.node.nodeCluster.id" title="点击查看节点详情" target="_top"><span class="grey">[{{accessLog.node.name}}<span v-if="!accessLog.node.name.endsWith('节点')">节点</span>]</span></a><span v-if="accessLog.region != null && accessLog.region.length > 0" class="grey">[{{accessLog.region}}]</span> <ip-box><keyword :v-word="vKeyword">{{accessLog.remoteAddr}}</keyword></ip-box> [{{accessLog.timeLocal}}] <em>&quot;<keyword :v-word="vKeyword">{{accessLog.requestMethod}}</keyword> {{accessLog.scheme}}://<keyword :v-word="vKeyword">{{accessLog.host}}</keyword><keyword :v-word="vKeyword">{{accessLog.requestURI}}</keyword> <a :href="accessLog.scheme + '://' + accessLog.host + accessLog.requestURI" target="_blank" title="新窗口打开" class="disabled"><i class="external icon tiny"></i> </a> {{accessLog.proto}}&quot; </em> <keyword :v-word="vKeyword">{{accessLog.status}}</keyword> <code-label v-if="accessLog.attrs != null && accessLog.attrs['cache.status'] == 'HIT'">cache hit</code-label> <code-label v-if="accessLog.firewallActions != null && accessLog.firewallActions.length > 0">waf {{accessLog.firewallActions}}</code-label> <span v-if="accessLog.tags != null && accessLog.tags.length > 0">- <code-label v-for="tag in accessLog.tags" :key="tag">{{tag}}</code-label></span> - 耗时:{{formatCost(accessLog.requestTime)}} ms <span v-if="accessLog.humanTime != null && accessLog.humanTime.length > 0" class="grey small">&nbsp; ({{accessLog.humanTime}})</span>
<a v-if="accessLog.node != null && accessLog.node.nodeCluster != null" :href="'/clusters/cluster/node?nodeId=' + accessLog.node.id + '&clusterId=' + accessLog.node.nodeCluster.id" title="点击查看节点详情" target="_top"><span class="grey">[{{accessLog.node.name}}<span v-if="!accessLog.node.name.endsWith('节点')">节点</span>]</span></a>
<a :href="'/servers/server/log?serverId=' + accessLog.serverId" title="点击到网站服务" v-if="vShowServerLink"><span class="grey">[服务]</span></a>
<span v-if="accessLog.region != null && accessLog.region.length > 0" class="grey">[{{accessLog.region}}]</span> <ip-box><keyword :v-word="vKeyword">{{accessLog.remoteAddr}}</keyword></ip-box> [{{accessLog.timeLocal}}] <em>&quot;<keyword :v-word="vKeyword">{{accessLog.requestMethod}}</keyword> {{accessLog.scheme}}://<keyword :v-word="vKeyword">{{accessLog.host}}</keyword><keyword :v-word="vKeyword">{{accessLog.requestURI}}</keyword> <a :href="accessLog.scheme + '://' + accessLog.host + accessLog.requestURI" target="_blank" title="新窗口打开" class="disabled"><i class="external icon tiny"></i> </a> {{accessLog.proto}}&quot; </em> <keyword :v-word="vKeyword">{{accessLog.status}}</keyword> <code-label v-if="accessLog.attrs != null && accessLog.attrs['cache.status'] == 'HIT'">cache hit</code-label> <code-label v-if="accessLog.firewallActions != null && accessLog.firewallActions.length > 0">waf {{accessLog.firewallActions}}</code-label> <span v-if="accessLog.tags != null && accessLog.tags.length > 0">- <code-label v-for="tag in accessLog.tags" :key="tag">{{tag}}</code-label></span> - 耗时:{{formatCost(accessLog.requestTime)}} ms <span v-if="accessLog.humanTime != null && accessLog.humanTime.length > 0" class="grey small">&nbsp; ({{accessLog.humanTime}})</span>
&nbsp; <a href="" @click.prevent="showLog" title="查看详情"><i class="icon expand"></i></a>
</div>`
})

View File

@@ -7,7 +7,9 @@ Vue.component("http-cache-config-box", {
isPrior: false,
isOn: false,
addStatusHeader: true,
cacheRefs: []
cacheRefs: [],
purgeIsOn: false,
purgeKey: ""
}
}
return {
@@ -17,6 +19,16 @@ Vue.component("http-cache-config-box", {
methods: {
isOn: function () {
return ((!this.vIsLocation && !this.vIsGroup) || this.cacheConfig.isPrior) && this.cacheConfig.isOn
},
generatePurgeKey: function () {
let r = Math.random().toString() + Math.random().toString()
let s = r.replace(/0\./g, "")
.replace(/\./g, "")
let result = ""
for (let i = 0; i < s.length; i++) {
result += String.fromCharCode(parseInt(s.substring(i, i + 1)) + ((Math.random() < 0.5) ? "a" : "A").charCodeAt(0))
}
this.cacheConfig.purgeKey = result
}
},
template: `<div>
@@ -51,6 +63,20 @@ Vue.component("http-cache-config-box", {
<p class="comment">选中后自动在响应Header中增加<code-label>X-Cache: BYPASS|MISS|HIT</code-label>。</p>
</td>
</tr>
<tr>
<td>允许PURGE</td>
<td>
<checkbox v-model="cacheConfig.purgeIsOn"></checkbox>
<p class="comment">允许使用PURGE方法清除某个URL缓存。</p>
</td>
</tr>
<tr v-show="cacheConfig.purgeIsOn">
<td>PURGE Key *</td>
<td>
<input type="text" maxlength="200" v-model="cacheConfig.purgeKey"/>
<p class="comment"><a href="" @click.prevent="generatePurgeKey">[随机生成]</a>。需要在PURGE方法调用时加入<code-label>Edge-Purge-Key: {{cacheConfig.purgeKey}}</code-label> Header。只能包含字符、数字、下划线。</p>
</td>
</tr>
</tbody>
</table>

View File

@@ -14,8 +14,9 @@ Vue.component("http-compression-config-box", {
isPrior: false,
isOn: false,
useDefaultTypes: true,
types: ["gzip", "deflate", "brotli"],
types: ["brotli", "gzip", "deflate"],
level: 5,
decompressData: false,
gzipRef: null,
deflateRef: null,
brotliRef: null,
@@ -188,7 +189,7 @@ Vue.component("http-compression-config-box", {
<td>
<div class="ui checkbox">
<input type="checkbox" v-model="config.useDefaultTypes" id="compression-use-default"/>
<label v-if="config.useDefaultTypes" for="compression-use-default">使用默认顺序<span class="grey small">gzip、deflate、brotli</span></label>
<label v-if="config.useDefaultTypes" for="compression-use-default">使用默认顺序<span class="grey small">brotli、gzip、deflate</span></label>
<label v-if="!config.useDefaultTypes" for="compression-use-default">使用默认顺序</label>
</div>
<div v-show="!config.useDefaultTypes">
@@ -204,26 +205,33 @@ Vue.component("http-compression-config-box", {
<p class="comment">选择支持的压缩算法和优先顺序,拖动<i class="icon list small grey"></i>图表排序。</p>
</td>
</tr>
<tr>
<td>支持已压缩内容</td>
<td>
<checkbox v-model="config.decompressData"></checkbox>
<p class="comment">支持对已压缩内容尝试重新使用新的算法压缩。</p>
</td>
</tr>
<tr>
<td>内容最小长度</td>
<td>
<size-capacity-box :v-name="'minLength'" :v-value="config.minLength" :v-unit="'kb'"></size-capacity-box>
<p class="comment">0表示不限制内容长度从文件尺寸或Content-Length中获取。</p>
</td>
</tr>
<tr>
<td>内容最大长度</td>
<td>
<size-capacity-box :v-name="'maxLength'" :v-value="config.maxLength" :v-unit="'mb'"></size-capacity-box>
<p class="comment">0表示不限制内容长度从文件尺寸或Content-Length中获取。</p>
</td>
</tr>
<tr>
<td>匹配条件</td>
<td>
<http-request-conds-box :v-conds="config.conds" @change="changeConds"></http-request-conds-box>
</td>
</tr>
<td>
<size-capacity-box :v-name="'minLength'" :v-value="config.minLength" :v-unit="'kb'"></size-capacity-box>
<p class="comment">0表示不限制内容长度从文件尺寸或Content-Length中获取。</p>
</td>
</tr>
<tr>
<td>内容最大长度</td>
<td>
<size-capacity-box :v-name="'maxLength'" :v-value="config.maxLength" :v-unit="'mb'"></size-capacity-box>
<p class="comment">0表示不限制内容长度从文件尺寸或Content-Length中获取。</p>
</td>
</tr>
<tr>
<td>匹配条件</td>
<td>
<http-request-conds-box :v-conds="config.conds" @change="changeConds"></http-request-conds-box>
</td>
</tr>
</tbody>
</table>
<div class="margin"></div>

View File

@@ -46,6 +46,14 @@ Vue.component("http-firewall-actions-box", {
})
}
var defaultPageBody = `<!DOCTYPE html>
<html>
<body>
403 Forbidden
</body>
</html>`
return {
id: id,
@@ -63,6 +71,8 @@ Vue.component("http-firewall-actions-box", {
// 动作参数
blockTimeout: "",
blockScope: "global",
captchaLife: "",
get302Life: "",
post307Life: "",
@@ -74,6 +84,10 @@ Vue.component("http-firewall-actions-box", {
tagTags: [],
pageStatus: 403,
pageBody: defaultPageBody,
defaultPageBody: defaultPageBody,
goGroupName: "",
goGroupId: 0,
goGroup: null,
@@ -97,6 +111,9 @@ Vue.component("http-firewall-actions-box", {
this.actionOptions["timeout"] = v
}
},
blockScope: function (v) {
this.actionOptions["scope"] = v
},
captchaLife: function (v) {
v = parseInt(v)
if (isNaN(v)) {
@@ -169,6 +186,7 @@ Vue.component("http-firewall-actions-box", {
// 动作参数
this.blockTimeout = ""
this.blockScope = "global"
this.captchaLife = ""
this.get302Life = ""
this.post307Life = ""
@@ -181,6 +199,9 @@ Vue.component("http-firewall-actions-box", {
this.tagTags = []
this.pageStatus = 403
this.pageBody = this.defaultPageBody
this.goGroupName = ""
this.goGroupId = 0
this.goGroup = null
@@ -220,6 +241,11 @@ Vue.component("http-firewall-actions-box", {
if (config.options.timeout != null || config.options.timeout > 0) {
this.blockTimeout = config.options.timeout.toString()
}
if (config.options.scope != null && config.options.scope.length > 0) {
this.blockScope = config.options.scope
} else {
this.blockScope = "service" // 兼容先前版本遗留的默认值
}
break
case "allow":
break
@@ -266,6 +292,17 @@ Vue.component("http-firewall-actions-box", {
if (config.options.tags != null) {
this.tagTags = config.options.tags
}
break
case "page":
this.pageStatus = 403
this.pageBody = this.defaultPageBody
if (config.options.status != null) {
this.pageStatus = config.options.status
}
if (config.options.body != null) {
this.pageBody = config.options.body
}
break
case "go_group":
if (config.options != null) {
@@ -340,6 +377,18 @@ Vue.component("http-firewall-actions-box", {
this.actionOptions = {
tags: this.tagTags
}
} else if (this.actionCode == "page") {
let pageStatus = this.pageStatus.toString()
if (!pageStatus.match(/^\d{3}$/)) {
pageStatus = 403
} else {
pageStatus = parseInt(pageStatus)
}
this.actionOptions = {
status: pageStatus,
body: this.pageBody
}
} else if (this.actionCode == "go_group") { // go_group
let groupId = this.goGroupId
if (typeof (groupId) == "string") {
@@ -450,7 +499,7 @@ Vue.component("http-firewall-actions-box", {
<input type="hidden" name="actionsJSON" :value="JSON.stringify(configs)"/>
<div v-show="configs.length > 0" style="margin-bottom: 0.5em" id="actions-box">
<div v-for="(config, index) in configs" :data-index="index" :key="config.id" class="ui label small basic" :class="{blue: index == editingIndex}" style="margin-bottom: 0.4em">
{{config.name}} ({{config.code.toUpperCase()}})
{{config.name}} <span class="small">({{config.code.toUpperCase()}})</span>
<!-- block -->
<span v-if="config.code == 'block' && config.options.timeout > 0">:有效期{{config.options.timeout}}秒</span>
@@ -470,12 +519,22 @@ Vue.component("http-firewall-actions-box", {
<!-- tag -->
<span v-if="config.code == 'tag'">{{config.options.tags.join(", ")}}</span>
<!-- page -->
<span v-if="config.code == 'page'">[{{config.options.status}}]</span>
<!-- go_group -->
<span v-if="config.code == 'go_group'">{{config.options.groupName}}</span>
<!-- go_set -->
<span v-if="config.code == 'go_set'">{{config.options.groupName}} / {{config.options.setName}}</span>
<!-- 范围 -->
<span v-if="config.options.scope != null && config.options.scope.length > 0" class="small grey">
&nbsp;
<span v-if="config.options.scope == 'global'">[所有服务]</span>
<span v-if="config.options.scope == 'service'">[当前服务]</span>
</span>
<!-- 操作按钮 -->
&nbsp; <a href="" title="修改" @click.prevent="update(index, config)"><i class="icon pencil small"></i></a> &nbsp; <a href="" title="删除" @click.prevent="remove(index)"><i class="icon remove small"></i></a> &nbsp; <a href="" title="拖动改变顺序"><i class="icon bars handle"></i></a>
</div>
@@ -503,6 +562,15 @@ Vue.component("http-firewall-actions-box", {
</div>
</td>
</tr>
<tr v-if="actionCode == 'block'">
<td>封锁范围</td>
<td>
<select class="ui dropdown auto-width" v-model="blockScope">
<option value="service">当前服务</option>
<option value="global">所有服务</option>
</select>
</td>
</tr>
<!-- captcha -->
<tr v-if="actionCode == 'captcha'">
@@ -585,6 +653,18 @@ Vue.component("http-firewall-actions-box", {
</td>
</tr>
<!-- page -->
<tr v-if="actionCode == 'page'">
<td>状态码 *</td>
<td><input type="text" style="width: 4em" maxlength="3" v-model="pageStatus"/></td>
</tr>
<tr v-if="actionCode == 'page'">
<td>网页内容</td>
<td>
<textarea v-model="pageBody"></textarea>
</td>
</tr>
<!-- 规则分组 -->
<tr v-if="actionCode == 'go_group'">
<td>下一个分组 *</td>

View File

@@ -21,6 +21,12 @@ Vue.component("http-firewall-rule-label", {
<span v-if="rule.param == '\${cc2}'">
{{rule.checkpointOptions.period}}秒/{{rule.checkpointOptions.threshold}}请求
</span>
<!-- refererBlock -->
<span v-if="rule.param == '\${refererBlock}'">
{{rule.checkpointOptions.allowDomains}}
</span>
<span v-else>
<span v-if="rule.paramFilters != null && rule.paramFilters.length > 0" v-for="paramFilter in rule.paramFilters"> | {{paramFilter.code}}</span>
<var :class="{dash:rule.isCaseInsensitive}" :title="rule.isCaseInsensitive ? '大小写不敏感':''" v-if="!rule.isComposed">{{rule.operator}}</var>

View File

@@ -45,6 +45,12 @@ Vue.component("http-firewall-rules-box", {
<span v-if="rule.param == '\${cc2}'">
{{rule.checkpointOptions.period}}秒/{{rule.checkpointOptions.threshold}}请求
</span>
<!-- refererBlock -->
<span v-if="rule.param == '\${refererBlock}'">
{{rule.checkpointOptions.allowDomains}}
</span>
<span v-else>
<span v-if="rule.paramFilters != null && rule.paramFilters.length > 0" v-for="paramFilter in rule.paramFilters"> | {{paramFilter.code}}</span> <var>{{rule.operator}}</var> {{rule.value}}
</span>

View File

@@ -195,4 +195,102 @@ Vue.component("http-firewall-checkpoint-cc", {
</tr>
</table>
</div>`
})
// 防盗链
Vue.component("http-firewall-checkpoint-referer-block", {
props: ["v-checkpoint"],
data: function () {
let allowEmpty = true
let allowSameDomain = true
let allowDomains = []
let options = {}
if (window.parent.UPDATING_RULE != null) {
options = window.parent.UPDATING_RULE.checkpointOptions
}
if (options == null) {
options = {}
}
if (typeof (options.allowEmpty) == "boolean") {
allowEmpty = options.allowEmpty
}
if (typeof (options.allowSameDomain) == "boolean") {
allowSameDomain = options.allowSameDomain
}
if (options.allowDomains != null && typeof (options.allowDomains) == "object") {
allowDomains = options.allowDomains
}
let that = this
setTimeout(function () {
that.change()
}, 100)
return {
allowEmpty: allowEmpty,
allowSameDomain: allowSameDomain,
allowDomains: allowDomains,
options: {},
value: 0
}
},
watch: {
allowEmpty: function () {
this.change()
},
allowSameDomain: function () {
this.change()
}
},
methods: {
changeAllowDomains: function (values) {
this.allowDomains = values
this.change()
},
change: function () {
this.vCheckpoint.options = [
{
code: "allowEmpty",
value: this.allowEmpty
},
{
code: "allowSameDomain",
value: this.allowSameDomain,
},
{
code: "allowDomains",
value: this.allowDomains
}
]
}
},
template: `<div>
<input type="hidden" name="operator" value="eq"/>
<input type="hidden" name="value" :value="value"/>
<table class="ui table">
<tr>
<td class="title">来源域名允许为空</td>
<td>
<checkbox v-model="allowEmpty"></checkbox>
<p class="comment">允许不带来源的访问。</p>
</td>
</tr>
<tr>
<td>来源域名允许一致</td>
<td>
<checkbox v-model="allowSameDomain"></checkbox>
<p class="comment">允许来源域名和当前访问的域名一致,相当于在站内访问。</p>
</td>
</tr>
<tr>
<td>允许的来源域名</td>
<td>
<values-box :values="allowDomains" @change="changeAllowDomains"></values-box>
<p class="comment">允许的来源域名列表,比如<code-label>example.com</code-label>、<code-label>*.example.com</code-label>。单个星号<code-label>*</code-label>表示允许所有域名。</p>
</td>
</tr>
</table>
</div>`
})

View File

@@ -6,16 +6,49 @@ Vue.component("http-remote-addr-config-box", {
config = {
isPrior: false,
isOn: false,
value: "${remoteAddr}"
value: "${rawRemoteAddr}",
isCustomized: false
}
}
let optionValue = ""
if (!config.isCustomized && (config.value == "${remoteAddr}" || config.value == "${rawRemoteAddr}")) {
optionValue = config.value
}
return {
config: config
config: config,
options: [
{
name: "直接获取",
description: "用户直接访问边缘节点,即 \"用户 --> 边缘节点\" 模式这时候可以直接从连接中读取到真实的IP地址。",
value: "${rawRemoteAddr}"
},
{
name: "从上级代理中获取",
description: "用户和边缘节点之间有别的代理服务转发,即 \"用户 --> [第三方代理服务] --> 边缘节点\"这时候只能从上级代理中获取传递的IP地址。",
value: "${remoteAddr}"
},
{
name: "[自定义]",
description: "通过自定义变量来获取客户端真实的IP地址。",
value: ""
}
],
optionValue: optionValue
}
},
methods: {
isOn: function () {
return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn
},
changeOptionValue: function () {
if (this.optionValue.length > 0) {
this.config.value = this.optionValue
this.config.isCustomized = false
} else {
this.config.isCustomized = true
}
}
},
template: `<div>
@@ -36,10 +69,22 @@ Vue.component("http-remote-addr-config-box", {
</tbody>
<tbody v-show="isOn()">
<tr>
<td>请求变量</td>
<td>获取IP方式 *</td>
<td>
<input type="text" v-model="config.value" maxlength="100"/>
<p class="comment">通过此变量获取用户的IP地址。具体可用的请求变量列表可参考官方网站文档。</p>
<select class="ui dropdown auto-width" v-model="optionValue" @change="changeOptionValue">
<option v-for="option in options" :value="option.value">{{option.name}}</option>
</select>
<p class="comment" v-for="option in options" v-if="option.value == optionValue && option.description.length > 0">{{option.description}}</p>
</td>
</tr>
<tr v-show="optionValue.length == 0">
<td>读取IP变量值 *</td>
<td>
<input type="hidden" v-model="config.value" maxlength="100"/>
<div v-if="optionValue == ''" style="margin-top: 1em">
<input type="text" v-model="config.value" maxlength="100"/>
<p class="comment">通过此变量获取用户的IP地址。具体可用的请求变量列表可参考官方网站文档。</p>
</div>
</td>
</tr>
</tbody>

View File

@@ -9,8 +9,8 @@ Vue.component("http-webp-config-box", {
quality: 50,
minLength: {count: 0, "unit": "kb"},
maxLength: {count: 0, "unit": "kb"},
mimeTypes: ["image/*"],
extensions: [".png", ".jpeg", ".jpg"],
mimeTypes: ["image/png", "image/jpeg", "image/bmp", "image/x-ico", "image/gif"],
extensions: [".png", ".jpeg", ".jpg", ".bmp", ".ico"],
conds: null
}
}

View File

@@ -39,6 +39,14 @@ Vue.component("reverse-proxy-box", {
reverseProxyConfig.idleTimeout = {count: 0, unit: "second"}
}
if (reverseProxyConfig.proxyProtocol == null) {
// 如果直接赋值Vue将不会触发变更通知
Vue.set(reverseProxyConfig, "proxyProtocol", {
isOn: false,
version: 1
})
}
let forwardHeaders = [
{
name: "X-Real-IP",
@@ -116,6 +124,13 @@ Vue.component("reverse-proxy-box", {
}
this.reverseProxyConfig.maxIdleConns = maxIdleConns
},
"reverseProxyConfig.proxyProtocol.version": function (v) {
let version = parseInt(v)
if (isNaN(version)) {
version = 1
}
this.reverseProxyConfig.proxyProtocol.version = version
}
},
methods: {
isOn: function () {
@@ -194,7 +209,7 @@ Vue.component("reverse-proxy-box", {
<p class="comment">可以把请求的路径部分前缀去除后再查找文件,比如把 <span class="ui label tiny">/web/app/index.html</span> 去除前缀 <span class="ui label tiny">/web</span> 后就变成 <span class="ui label tiny">/app/index.html</span>。 </p>
</td>
</tr>
<tr>
<tr v-if="family == null || family == 'http'">
<td>是否自动刷新缓存区<em>AutoFlush</em></td>
<td>
<div class="ui checkbox">
@@ -205,7 +220,7 @@ Vue.component("reverse-proxy-box", {
</td>
</tr>
<tr v-if="family == null || family == 'http'">
<td class="color-border">源站默认连接失败超时时间</td>
<td class="color-border">源站连接失败超时时间</td>
<td>
<div class="ui fields inline">
<div class="ui field">
@@ -219,7 +234,7 @@ Vue.component("reverse-proxy-box", {
</td>
</tr>
<tr v-if="family == null || family == 'http'">
<td class="color-border">源站默认读取超时时间</td>
<td class="color-border">源站读取超时时间</td>
<td>
<div class="ui fields inline">
<div class="ui field">
@@ -233,7 +248,7 @@ Vue.component("reverse-proxy-box", {
</td>
</tr>
<tr v-if="family == null || family == 'http'">
<td class="color-border">源站默认最大并发连接数</td>
<td class="color-border">源站最大并发连接数</td>
<td>
<div class="ui fields inline">
<div class="ui field">
@@ -244,7 +259,7 @@ Vue.component("reverse-proxy-box", {
</td>
</tr>
<tr v-if="family == null || family == 'http'">
<td class="color-border">源站默认最大空闲连接数</td>
<td class="color-border">源站最大空闲连接数</td>
<td>
<div class="ui fields inline">
<div class="ui field">
@@ -255,7 +270,7 @@ Vue.component("reverse-proxy-box", {
</td>
</tr>
<tr v-if="family == null || family == 'http'">
<td class="color-border">源站默认最大空闲超时时间</td>
<td class="color-border">源站最大空闲超时时间</td>
<td>
<div class="ui fields inline">
<div class="ui field">
@@ -268,6 +283,24 @@ Vue.component("reverse-proxy-box", {
<p class="comment">源站保持等待的空闲超时时间0表示使用默认时间。</p>
</td>
</tr>
<tr v-show="family != 'unix'">
<td>PROXY Protocol</td>
<td>
<checkbox name="proxyProtocolIsOn" v-model="reverseProxyConfig.proxyProtocol.isOn"></checkbox>
<p class="comment">选中后表示启用PROXY Protocol每次连接源站时都会在头部写入客户端地址信息。</p>
</td>
</tr>
<tr v-show="family != 'unix' && reverseProxyConfig.proxyProtocol.isOn">
<td>PROXY Protocol版本</td>
<td>
<select class="ui dropdown auto-width" name="proxyProtocolVersion" v-model="reverseProxyConfig.proxyProtocol.version">
<option value="1">1</option>
<option value="2">2</option>
</select>
<p class="comment" v-if="reverseProxyConfig.proxyProtocol.version == 1">发送类似于<code-label>PROXY TCP4 192.168.1.1 192.168.1.10 32567 443</code-label>的头部信息。</p>
<p class="comment" v-if="reverseProxyConfig.proxyProtocol.version == 2">发送二进制格式的头部信息。</p>
</td>
</tr>
</tbody>
</table>
<div class="margin"></div>

View File

@@ -69,13 +69,13 @@ window.teaweb = {
}
document.head.append(element)
},
datepicker: function (element, callback) {
datepicker: function (element, callback, bottomLeft) {
// 加载
if (typeof Pikaday == "undefined") {
let that = this
this.loadJS("/js/moment.min.js", function () {
that.loadJS("/js/pikaday.js", function () {
that.datepicker(element, callback)
that.datepicker(element, callback, bottomLeft)
})
})
this.loadCSS("/js/pikaday.css")
@@ -89,8 +89,8 @@ window.teaweb = {
if (typeof (element) == "string") {
element = document.getElementById(element);
}
var year = new Date().getFullYear();
var picker = new Pikaday({
let year = new Date().getFullYear();
let picker = new Pikaday({
field: element,
firstDay: 1,
minDate: new Date(year - 1, 0, 1),
@@ -109,8 +109,9 @@ window.teaweb = {
if (typeof (callback) == "function") {
callback.call(Tea.Vue, picker.toString());
}
}
});
},
reposition: !bottomLeft
})
},
formatBytes: function (bytes) {
@@ -256,6 +257,21 @@ window.teaweb = {
}
});
},
popupSuccess: function (url, width, height) {
let options = {}
if (width != null) {
options["width"] = width
}
if (height != null) {
options["height"] = height
}
options["callback"] = function () {
teaweb.success("保存成功", function () {
teaweb.reload()
})
}
this.popup(url, options)
},
popupFinish: function () {
if (window.POPUP_CALLBACK != null) {
window.POPUP_CALLBACK.apply(window, arguments);

View File

@@ -239,12 +239,20 @@ p.margin {
}
.main-menu .ui.menu .sub-items .item {
padding-left: 2.8em !important;
padding-right: 0.4em !important;
}
.main-menu .ui.menu .sub-items .item .icon {
position: absolute;
left: 1.1em;
top: 0.93em;
}
.main-menu .ui.menu .sub-items .item .label {
margin-left: 0;
margin-right: 0;
padding-left: 0.4em;
padding-right: 0.4em;
min-width: 2em;
}
@media screen and (max-width: 512px) {
.main-menu .ui.menu .sub-items .item {
padding-left: 1em !important;

File diff suppressed because one or more lines are too long

View File

@@ -92,7 +92,9 @@
<div class="subtitle" v-if="module.subtitle != null && module.subtitle.length > 0">{{module.subtitle}}</div>
</a>
<div v-if="teaMenu == module.code" class="sub-items">
<a class="item" v-for="subItem in module.subItems" v-if="subItem.isOn !== false" :href="subItem.url"><i class="icon angle right" v-if="subItem.code == teaSubMenu"></i> {{subItem.name}}</a>
<a class="item" v-for="subItem in module.subItems" v-if="subItem.isOn !== false" :href="subItem.url"><i class="icon angle right" v-if="subItem.code == teaSubMenu"></i> {{subItem.name}}
<span class="ui label tiny red" v-if="subItem.badge != null && subItem.badge > 0">{{subItem.badge}}</span>
</a>
</div>
</div>
</div>

View File

@@ -1,25 +1,27 @@
Tea.context(function () {
this.moreOptionsVisible = false
this.globalMessageBadge = 0
this.moreOptionsVisible = false
this.globalMessageBadge = 0
if (typeof this.leftMenuItemIsDisabled == "undefined") {
this.leftMenuItemIsDisabled = false
}
if (typeof this.leftMenuItemIsDisabled == "undefined") {
this.leftMenuItemIsDisabled = false
}
this.$delay(function () {
if (this.$refs.focus != null) {
this.$refs.focus.focus()
}
this.$delay(function () {
if (this.$refs.focus != null) {
this.$refs.focus.focus()
}
// 检查消息
this.checkMessages()
if (!window.IS_POPUP) {
// 检查消息
this.checkMessages()
// 检查集群节点同步
this.loadNodeTasks();
// 检查集群节点同步
this.loadNodeTasks();
// 检查DNS同步
this.loadDNSTasks()
})
// 检查DNS同步
this.loadDNSTasks()
}
})
/**
* 切换模板
@@ -32,186 +34,192 @@ Tea.context(function () {
})
}
/**
* 左侧子菜单
*/
this.showSubMenu = function (menu) {
if (menu.alwaysActive) {
return
}
if (this.teaSubMenus.menus != null && this.teaSubMenus.menus.length > 0) {
this.teaSubMenus.menus.$each(function (k, v) {
if (menu.id == v.id) {
return
}
v.isActive = false
})
}
menu.isActive = !menu.isActive
};
/**
* 左侧子菜单
*/
this.showSubMenu = function (menu) {
if (menu.alwaysActive) {
return
}
if (this.teaSubMenus.menus != null && this.teaSubMenus.menus.length > 0) {
this.teaSubMenus.menus.$each(function (k, v) {
if (menu.id == v.id) {
return
}
v.isActive = false
})
}
menu.isActive = !menu.isActive
};
/**
* 检查消息
*/
this.checkMessages = function () {
this.$post("/messages/badge")
.params({})
.success(function (resp) {
this.globalMessageBadge = resp.data.count
})
.done(function () {
let delay = 6000
if (this.globalMessageBadge > 0) {
delay = 30000
}
this.$delay(function () {
this.checkMessages()
}, delay)
})
}
/**
* 检查消息
*/
this.checkMessages = function () {
this.$post("/messages/badge")
.params({})
.success(function (resp) {
this.globalMessageBadge = resp.data.count
})
.done(function () {
let delay = 6000
if (this.globalMessageBadge > 0) {
delay = 30000
}
this.$delay(function () {
this.checkMessages()
}, delay)
})
}
this.checkMessagesOnce = function () {
this.$post("/messages/badge")
.params({})
.success(function (resp) {
this.globalMessageBadge = resp.data.count
})
}
this.checkMessagesOnce = function () {
this.$post("/messages/badge")
.params({})
.success(function (resp) {
this.globalMessageBadge = resp.data.count
})
}
this.showMessages = function () {
teaweb.popup("/messages", {
height: "24em",
width: "50em"
})
}
this.showMessages = function () {
teaweb.popup("/messages", {
height: "24em",
width: "50em"
})
}
/**
* 底部伸展框
*/
this.showQQGroupQrcode = function () {
teaweb.popup("/about/qq", {
width: "21em",
height: "24em"
})
}
/**
* 底部伸展框
*/
this.showQQGroupQrcode = function () {
teaweb.popup("/about/qq", {
width: "21em",
height: "24em"
})
}
/**
* 弹窗中默认成功回调
*/
if (window.IS_POPUP === true) {
this.success = window.NotifyPopup
}
/**
* 弹窗中默认成功回调
*/
if (window.IS_POPUP === true) {
this.success = window.NotifyPopup
}
/**
* 节点同步任务
*/
this.doingNodeTasks = {
isDoing: false,
hasError: false,
isUpdated: false
}
/**
* 节点同步任务
*/
this.doingNodeTasks = {
isDoing: false,
hasError: false,
isUpdated: false
}
this.loadNodeTasks = function () {
if (!Tea.Vue.teaCheckNodeTasks) {
return
}
this.$post("/clusters/tasks/check")
.success(function (resp) {
this.doingNodeTasks.isDoing = resp.data.isDoing
this.doingNodeTasks.hasError = resp.data.hasError
this.doingNodeTasks.isUpdated = true
})
.done(function () {
this.$delay(function () {
this.loadNodeTasks()
}, 3000)
})
}
this.loadNodeTasks = function () {
if (!Tea.Vue.teaCheckNodeTasks) {
return
}
this.$post("/clusters/tasks/check")
.success(function (resp) {
this.doingNodeTasks.isDoing = resp.data.isDoing
this.doingNodeTasks.hasError = resp.data.hasError
this.doingNodeTasks.isUpdated = true
})
.done(function () {
this.$delay(function () {
this.loadNodeTasks()
}, 3000)
})
}
this.showNodeTasks = function () {
teaweb.popup("/clusters/tasks/listPopup", {
height: "24em",
width: "50em"
})
}
this.showNodeTasks = function () {
teaweb.popup("/clusters/tasks/listPopup", {
height: "24em",
width: "50em"
})
}
/**
* DNS同步任务
*/
this.doingDNSTasks = {
isDoing: false,
hasError: false,
isUpdated: false
}
/**
* DNS同步任务
*/
this.doingDNSTasks = {
isDoing: false,
hasError: false,
isUpdated: false
}
this.loadDNSTasks = function () {
if (!Tea.Vue.teaCheckDNSTasks) {
return
}
this.$post("/dns/tasks/check")
.success(function (resp) {
this.doingDNSTasks.isDoing = resp.data.isDoing
this.doingDNSTasks.hasError = resp.data.hasError
this.doingDNSTasks.isUpdated = true
})
.done(function () {
this.$delay(function () {
this.loadDNSTasks()
}, 3000)
})
}
this.loadDNSTasks = function () {
if (!Tea.Vue.teaCheckDNSTasks) {
return
}
this.$post("/dns/tasks/check")
.success(function (resp) {
this.doingDNSTasks.isDoing = resp.data.isDoing
this.doingDNSTasks.hasError = resp.data.hasError
this.doingDNSTasks.isUpdated = true
})
.done(function () {
this.$delay(function () {
this.loadDNSTasks()
}, 3000)
})
}
this.showDNSTasks = function () {
teaweb.popup("/dns/tasks/listPopup", {
height: "24em",
width: "50em"
})
}
this.showDNSTasks = function () {
teaweb.popup("/dns/tasks/listPopup", {
height: "24em",
width: "50em"
})
}
});
window.NotifySuccess = function (message, url, params) {
if (typeof (url) == "string" && url.length > 0) {
if (url[0] != "/") {
url = Tea.url(url, params);
}
}
return function () {
teaweb.success(message, function () {
window.location = url;
});
};
if (typeof (url) == "string" && url.length > 0) {
if (url[0] != "/") {
url = Tea.url(url, params);
}
}
return function () {
teaweb.success(message, function () {
window.location = url;
});
};
};
window.NotifyReloadSuccess = function (message) {
return function () {
teaweb.success(message, function () {
window.location.reload()
})
}
return function () {
teaweb.success(message, function () {
window.location.reload()
})
}
}
window.NotifyDelete = function (message, url, params) {
teaweb.confirm(message, function () {
Tea.Vue.$post(url)
.params(params)
.refresh();
});
teaweb.confirm(message, function () {
Tea.Vue.$post(url)
.params(params)
.refresh();
});
};
window.NotifyPopup = function (resp) {
window.parent.teaweb.popupFinish(resp);
window.parent.teaweb.popupFinish(resp);
};
window.ChangePageSize = function (size) {
let url = window.location.toString();
if (url.indexOf("pageSize") > 0) {
url = url.replace(/pageSize=\d+/g, "pageSize=" + size);
} else {
if (url.indexOf("?") > 0) {
url += "&pageSize=" + size;
} else {
url += "?pageSize=" + size;
}
}
window.location = url;
let url = window.location.toString();
url = url.replace(/page=\d+/g, "page=1")
if (url.indexOf("pageSize") > 0) {
url = url.replace(/pageSize=\d+/g, "pageSize=" + size)
} else {
if (url.indexOf("?") > 0) {
let anchorIndex = url.indexOf("#")
if (anchorIndex < 0) {
url += "&pageSize=" + size;
} else {
url = url.substring(0, anchorIndex) + "&pageSize=" + size + url.substr(anchorIndex);
}
} else {
url += "?pageSize=" + size;
}
}
window.location = url;
};

View File

@@ -179,12 +179,21 @@ div.margin, p.margin {
.sub-items {
.item {
padding-left: 2.8em !important;
padding-right: 0.4em !important;
.icon {
position: absolute;
left: 1.1em;
top: 0.93em;
}
.label {
margin-left: 0;
margin-right: 0;
padding-left: 0.4em;
padding-right: 0.4em;
min-width: 2em;
}
}
@media screen and (max-width: 512px) {

View File

@@ -54,6 +54,7 @@ p.comment,
div.comment {
color: rgba(0, 0, 0, 0.4);
padding-top: 0.4em;
font-weight: normal;
}
p.comment em,
div.comment em {

View File

@@ -1 +1 @@
{"version":3,"sources":["@layout_popup.less"],"names":[],"mappings":"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","file":"@layout_popup.css"}
{"version":3,"sources":["@layout_popup.less"],"names":[],"mappings":";AACA;EACC,WAAA;;AAGD;EACC,aAAA;;AAGD;EACC,qBAAA;;AAGD,CAAC;AAAW,CAAC,SAAS;AAAQ,CAAC,SAAS;AAAS,IAAI;EACpD,sBAAA;;AAGD,CAAC;AAAU,IAAI;AAAU,IAAI;EAC5B,cAAA;;AAGD,IAAI;AAAO,KAAK;AAAO,CAAC;EACvB,sBAAA;;AAGD,CAAC;EACA,iBAAA;;AAGD,IAAI;AAAM,GAAG;EACZ,cAAA;;AAGD,GAAG,IAAI;EACN,mBAAmB,8CAAnB;;AAGD;EACC,uBAAA;;AAGD,MAAM;EACL,sBAAA;;AAGD,MAAM;EACL,sBAAA;;AAGD,MAAM;EACL,sBAAA;;AAGD,MAAO;AAAI,MAAO;EACjB,2BAAA;;AAGD,CAAC;AAAU,GAAG;EACb,yBAAA;EACA,kBAAA;EACA,mBAAA;;AAGD,CAAC,QAAS;AAAI,GAAG,QAAS;EACzB,6BAAA;;AAGD;EACC,mBAAA;EACA,2BAAA;EACA,gBAAA;EACA,uBAAA;;AAGD,GAAG;AAAS,CAAC;EACZ,eAAA;;;AAID,GAAG;EACF,UAAA;;AAGD,GAAG;EACF,YAAA;;AAGD,GAAG;EACF,UAAA;;AAGD,GAAG;EACF,WAAA;;;AAID,MAAM;EACL,aAAA;;;AAID;EACC,kBAAA;EACA,UAAA;EACA,UAAA;EACA,mBAAA;EACA,kBAAA;EACA,UAAA;;AASD,mBANqC;EACpC;IACC,SAAA;;;AAIF,KAAK;EACJ,SAAA;;AAGD,KAAK;EACJ,UAAA;;AASD,mBANqC;EACpC,KAAK;IACJ,SAAA;;;AAIF,KAAM,MAAM,GAAE;EACb,WAAA;;AAGD,KAAM,MAAM,GAAE;EACb,WAAA;;AAGD,KAAM,MAAM;EACX,mBAAA;;AAGD,KAAM,MAAM,GAAE;EACb,yCAAA;;AAGD,KAAM,MAAM,GAAE;EACb,mBAAA;;AAGD,KAAM,MAAM,GAAE;EACb,sBAAA;;AAGD,KAAM,MAAM,GAAE,aAAc;EAC3B,mBAAA;;AAGD,KAAM,MAAM,GAAG;EACd,mBAAA;EACA,kBAAA;EACA,gBAAA;;AAGD,KAAM;EACL,mBAAA;EACA,4BAAA;;AAGD,KAAM,GAAG;EACR,gBAAA;;AAGD,KAAM,GAAG,KAAI;EACZ,cAAA;;AAGD,KAAM,GAAG;EACR,gBAAA;EACA,0BAAA;EACA,UAAA;;AAGD,KAAM,GAAG,EAAC;EACT,SAAS,GAAT;;AAGD,KAAM,GAAG,EAAC;EACT,SAAS,GAAT;;AAGD,KAAM;EACL,mBAAA;;AAGD,KAAM,GAAG,KAAI;EACZ,gBAAA;;AAGD,KAAM,QAAO;EACZ,gBAAA;EACA,cAAA;EACA,gBAAA;;;AAID,KAAK;EACJ,gBAAA;;AAGD,KAAK,KAAK;EACT,UAAA;EACA,WAAA;;;AAID;EACC,wBAAA;;;AAID,iBAAkB;EACjB,2BAAA;;AAGD,iBAAkB,MAAK;EACtB,UAAA;;AAGD,iBAAkB,MAAM;EACvB,2BAAA;;AAGD,MAAM;EACL,sBAAA;;;AAWD,mBAPqC;EACpC,OAAO,IAAI;IACV,sBAAA;;;;AAKF,KAAK;EACJ,0BAAA;;AAGD,KAAK;EACJ,yBAAA;;;AAOD,WAAY,MAAK;EAChB,wBAAA;EACA,2BAAA;;AAGD,WAAY;EACX,wBAAA;EACA,2BAAA;;AAGD,YAAa,MAAK;EACjB,wBAAA;EACA,2BAAA;;AAGD,YAAa,MAAK,KAAM;EACvB,kBAAA;;AAGD,YAAa;EACZ,wBAAA;;AAGD,KAAM;EACL,aAAA;;;AAID,IAAI;AAAQ,GAAG;EACd,yBAAA;;AAGD,GAAG;EACF,8BAAA;;;AAID,QAAS;EACR,WAAA;EACA,kBAAA;;;AAID,SAAU,MAAM;AAAG,SAAU;EAC5B,2BAAA;;;AAID;EACC,eAAA;EAEA,2BAAA;;AAHD,KAKC;EACC,qBAAA;EACA,mBAAA;EACA,WAAA;EACA,iBAAA;EACA,SAAA;EACA,gBAAA;EACA,sBAAA;EACA,cAAA;;AAbF,KAgBC,EAAC;EACA,8BAAA;EACA,YAAA;;AAlBF,KAqBC,EAAC;EACA,gBAAA;;AAKF;EACC,kBAAA;;AAGD,cAAc;AAAQ,aAAa;AAAQ,YAAY;EACtD,iCAAA;;AAGD;AAAgB;AAAe;EAC9B,iCAAA;;AAGD;EACC,2BAAA;;AAID,KAAK;EACJ,oCAAA;;AAID,QAAQ;EACP,4BAA4B,wBAA5B;EACA,2BAAA","file":"@layout_popup.css"}

View File

@@ -58,6 +58,7 @@ tbody {
p.comment, div.comment {
color: rgba(0, 0, 0, 0.4);
padding-top: 0.4em;
font-weight: normal;
}
p.comment em, div.comment em {

View File

@@ -2,7 +2,6 @@
{$template "/left_menu"}
<div class="right-box">
<h3>基础设置</h3>
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
<input type="hidden" name="clusterId" :value="cluster.id"/>
<table class="ui table selectable definition">
@@ -24,6 +23,24 @@
<p class="comment">当节点没有单独设置安装目录时,默认使用此设置。如果集群和节点都没有设置安装目录,则使用<span class="ui label tiny">/$登录用户/edge-node</span> 目录。</p>
</td>
</tr>
<tr>
<td>时区</td>
<td>
<div class="ui fields inline">
<div class="ui field">
<select class="ui dropdown" v-model="timeZoneGroupCode">
<option v-for="timeZoneGroup in timeZoneGroups" :value="timeZoneGroup.code">{{timeZoneGroup.name}}</option>
</select>
</div>
<div class="ui field">
<select class="ui dropdown" name="timeZone" v-model="cluster.timeZone">
<option v-for="timeZoneLocation in timeZoneLocations" :value="timeZoneLocation.name" v-if="timeZoneLocation.group == timeZoneGroupCode">{{timeZoneLocation.name}} ({{timeZoneLocation.offset}})</option>
</select>
</div>
</div>
<p class="comment">节点记录日志使用的时区。</p>
</td>
</tr>
</table>
<submit-btn></submit-btn>
</form>

View File

@@ -1,3 +1,35 @@
Tea.context(function () {
this.success = NotifyReloadSuccess("保存成功")
this.timeZoneGroupCode = "asia"
if (this.timeZoneLocation != null) {
this.timeZoneGroupCode = this.timeZoneLocation.group
}
let oldTimeZoneGroupCode = this.timeZoneGroupCode
let oldTimeZoneName = ""
if (this.timeZoneLocation != null) {
oldTimeZoneName = this.timeZoneLocation.name
}
this.$delay(function () {
this.$watch("timeZoneGroupCode", function (groupCode) {
if (groupCode == oldTimeZoneGroupCode && oldTimeZoneName.length > 0) {
this.cluster.timeZone = oldTimeZoneName
return
}
let firstLocation = null
this.timeZoneLocations.forEach(function (v) {
if (firstLocation != null) {
return
}
if (v.group == groupCode) {
firstLocation = v
}
})
if (firstLocation != null) {
this.cluster.timeZone = firstLocation.name
}
})
})
})

View File

@@ -41,7 +41,12 @@
</tr>
</thead>
<tr v-for="cluster in clusters">
<td><a :href="'/clusters/cluster?clusterId=' + cluster.id"><keyword :v-word="keyword">{{cluster.name}}</keyword></a></td>
<td>
<a :href="'/clusters/cluster?clusterId=' + cluster.id"><keyword :v-word="keyword">{{cluster.name}}</keyword></a>
<div v-if="cluster.timeZone != null && cluster.timeZone.length > 0">
<grey-label>时区:{{cluster.timeZone}}</grey-label>
</div>
</td>
<td class="center">
<a :href="'/clusters/cluster/nodes?clusterId=' + cluster.id" v-if="cluster.countAllNodes > 0"><span :class="{red:cluster.countAllNodes > cluster.countActiveNodes}">{{cluster.countAllNodes}}</span></a>
<span class="disabled" v-else="">-</span>
@@ -66,7 +71,8 @@
<span v-else class="disabled">-</span>
</td>
<td>
<a :href="'/clusters/cluster?clusterId=' + cluster.id">详情</a>
<a :href="'/clusters/cluster?clusterId=' + cluster.id">详情</a> &nbsp;
<a :href="'/clusters/cluster/settings?clusterId=' + cluster.id">设置</a>
</td>
</tr>
</table>

View File

@@ -0,0 +1,4 @@
<first-menu>
<menu-item href="/clusters/logs" code="index">所有</menu-item>
<menu-item href="/clusters/logs?type=unread" code="unread">未读<span :class="{red: countUnreadLogs > 0}">({{countUnreadLogs}})</span></menu-item>
</first-menu>

View File

@@ -1,9 +1,11 @@
{$layout}
{$template "/datepicker"}
{$template "menu"}
<div class="margin"></div>
<form method="get" action="/clusters/logs" class="ui form" autocomplete="off">
<input type="hidden" name="type" :value="type"/>
<div class="ui fields inline">
<div class="ui field">
<input type="text" name="dayFrom" placeholder="开始日期" v-model="dayFrom" value="" style="width:8em" id="day-from-picker"/>
@@ -27,11 +29,18 @@
<button type="submit" class="ui button">查询</button>
</div>
<div class="ui field" v-if="dayFrom.length > 0 || dayTo.length > 0 || keyword.length > 0 || level.length > 0">
<a href="/clusters/logs">[清除条件]</a>
<a :href="'/clusters/logs?type=' + type">[清除条件]</a>
</div>
</div>
</form>
<div v-if="type == 'unread'">
<div class="ui divider" style="margin-bottom: 0"></div>
<second-menu v-if="logs.length > 0">
<a href="" class="item" @click.prevent="updatePageRead()">[本页已读]</a>
</second-menu>
</div>
<p class="comment" v-if="logs.length == 0">暂时还没有日志。</p>
<table class="ui table selectable celled" v-if="logs.length > 0">
@@ -40,6 +49,7 @@
<th>集群</th>
<th>节点</th>
<th>信息</th>
<th class="one op">操作</th>
</tr>
</thead>
<tr v-for="log in logs">
@@ -48,6 +58,9 @@
<td>
<node-log-row :v-log="log" :v-keyword="keyword"></node-log-row>
</td>
<td>
<a href="" @click.prevent="updateRead(log.id)" v-if="!log.isRead">已读</a>
</td>
</tr>
</table>

View File

@@ -3,4 +3,27 @@ Tea.context(function () {
teaweb.datepicker("day-from-picker")
teaweb.datepicker("day-to-picker")
})
this.updateRead = function (logId) {
this.$post(".readLogs")
.params({
logIds: [logId]
})
.success(function () {
teaweb.reload()
})
}
this.updatePageRead = function () {
let logIds = this.logs.map(function (v) {
return v.id
})
this.$post(".readLogs")
.params({
logIds: logIds
})
.success(function () {
teaweb.reload()
})
}
})

View File

@@ -3,4 +3,5 @@
<menu-item href="/dashboard/boards/waf" code="waf">WAF</menu-item>
<menu-item href="/dashboard/boards/dns" code="dns">DNS</menu-item>
<menu-item href="/dashboard/boards/user" code="user">用户</menu-item>
<menu-item href="/dashboard/boards/events" code="event">事件<span :class="{red: countEvents > 0}">({{countEvents}})</span></menu-item>
</first-menu>

View File

@@ -0,0 +1,66 @@
{$layout}
{$template "menu"}
<p class="comment" v-if="logs.length == 0">暂时还没有事件。</p>
<second-menu v-if="logs.length > 0">
<a href="" class="item" @click.prevent="updatePageRead()">[本页已读]</a>
</second-menu>
<table class="ui table selectable celled" v-if="logs.length > 0">
<thead>
<tr>
<th nowrap="">节点类型</th>
<th nowrap="">集群</th>
<th nowrap="">节点</th>
<th>信息</th>
<th class="one op">操作</th>
</tr>
</thead>
<tr v-for="log in logs">
<td>
<node-role-name :v-role="log.role"></node-role-name>
</td>
<td nowrap="">
<span v-if="log.role == 'node'">
<link-icon :href="'/clusters/cluster?clusterId=' + log.node.cluster.id">{{log.node.cluster.name}}</link-icon>
</span>
<span v-else-if="log.role == 'dns'">
<link-icon :href="'/ns/clusters/cluster?clusterId=' + log.node.cluster.id">{{log.node.cluster.name}}</link-icon>
</span>
<span v-else class="disabled">-</span>
</td>
<td nowrap="">
<span v-if="log.role == 'node'">
<link-icon :href="'/clusters/cluster/node?clusterId=' + log.node.cluster.id + '&nodeId=' + log.node.id">{{log.node.name}}</link-icon>
</span>
<span v-if="log.role == 'api'">
<link-icon :href="'/api/node?nodeId=' + log.node.id">{{log.node.name}}</link-icon>
</span>
<span v-if="log.role == 'dns'">
<link-icon :href="'/ns/clusters/cluster/node?clusterId=' + log.node.cluster.id + '&nodeId=' + log.node.id">{{log.node.name}}</link-icon>
</span>
<span v-if="log.role == 'database'">
<link-icon :href="'/db/node?nodeId=' + log.node.id">{{log.node.name}}</link-icon>
</span>
<span v-if="log.role == 'admin'">管理平台</span>
<span v-if="log.role == 'user'">
<link-icon :href="'/settings/userNodes/node?nodeId=' + log.node.id">{{log.node.name}}</link-icon>
</span>
<span v-if="log.role == 'monitor'">
<link-icon :href="'/settings/monitorNodes/node?nodeId=' + log.node.id">{{log.node.name}}</link-icon>
</span>
<span v-if="log.role == 'report'">
<link-icon :href="'/clusters/monitors/reporters/reporter?reporterId=' + log.node.id">{{log.node.name}}</link-icon>
</span>
</td>
<td>
<node-log-row :v-log="log" :v-keyword="keyword"></node-log-row>
</td>
<td>
<a href="" @click.prevent="updateRead(log.id)">已读</a>
</td>
</tr>
</table>
<div class="page" v-html="page"></div>

View File

@@ -0,0 +1,24 @@
Tea.context(function () {
this.updateRead = function (logId) {
this.$post(".readEvents")
.params({
logIds: [logId]
})
.success(function () {
teaweb.reload()
})
}
this.updatePageRead = function () {
let logIds = this.logs.map(function (v) {
return v.id
})
this.$post(".readEvents")
.params({
logIds: logIds
})
.success(function () {
teaweb.reload()
})
}
})

View File

@@ -1,4 +0,0 @@
<first-menu>
<menu-item href="/finance/bills" code="index">账单</menu-item>
<menu-item href="/finance/bills/generate" code="generate">生成账单</menu-item>
</first-menu>

View File

@@ -1,8 +0,0 @@
{$layout}
{$template "menu"}
<div class="margin"></div>
<div>
<button type="button" class="ui primary button" @click.prevent="generateBills()">生成上个月({{month}})账单</button>
<p class="comment">多次点击不会重复生成同样的账单。</p>
</div>

View File

@@ -1,16 +0,0 @@
Tea.context(function () {
this.generateBills = function () {
let that = this
teaweb.confirm("确定要生成上个月的账单吗?", function () {
that.$post(".generate")
.params({
month: that.month
})
.success(function () {
teaweb.success("生成成功", function () {
window.location = "/finance/bills"
})
})
})
}
})

View File

@@ -1,30 +0,0 @@
{$layout}
{$template "menu"}
<p class="comment" v-if="bills.length == 0">暂时还没有账单。</p>
<table class="ui table selectable" v-if="bills.length > 0">
<thead>
<tr>
<th class="width10">用户</th>
<th class="width10">月份</th>
<th class="width10">项目</th>
<th class="width10">金额</th>
<th>描述</th>
<th class="width10">已支付</th>
</tr>
</thead>
<tr v-for="bill in bills">
<td>{{bill.user.fullname}}</td>
<td>{{bill.month}}</td>
<td>{{bill.typeName}}</td>
<td>¥{{bill.amount}}元</td>
<td>{{bill.description}}</td>
<td>
<span class="green" v-if="bill.isPaid">Y</span>
<span v-else class="disabled">N</span>
</td>
</tr>
</table>
<div class="page" v-html="page"></div>

View File

@@ -9,7 +9,7 @@
<menu-item :href="'/servers/certs/acme?type=available&keyword=' + keyword" :active="type == 'available'">有效证书({{countAvailable}})</menu-item>
<menu-item :href="'/servers/certs/acme?type=expired&keyword=' + keyword" :active="type == 'expired'">过期证书<span :class="{red: countExpired > 0}">({{countExpired}})</span></menu-item>
<menu-item :href="'/servers/certs/acme?type=7days&keyword=' + keyword" :active="type == '7days'">7天内过期<span :class="{red: count7Days > 0}">({{count7Days}})</span></menu-item>
<menu-item :href="'/servers/certs/acme?type=30days&keyword=' + keyword" :active="type == '30days'">30天过期({{count30Days}})</menu-item>
<menu-item :href="'/servers/certs/acme?type=30days&keyword=' + keyword" :active="type == '30days'">30天过期({{count30Days}})</menu-item>
<div class="item right">
<form class="ui form">
<div class="ui fields inline">

View File

@@ -8,8 +8,8 @@
<menu-item :href="'/servers/certs?type=available&keyword=' + keyword" :active="type == 'available'">有效证书({{countAvailable}})</menu-item>
<menu-item :href="'/servers/certs?type=expired&keyword=' + keyword" :active="type == 'expired'">过期证书<span :class="{red: countExpired > 0}">({{countExpired}})</span></menu-item>
<menu-item :href="'/servers/certs?type=7days&keyword=' + keyword" :active="type == '7days'">7天内过期<span :class="{red: count7Days > 0}">({{count7Days}})</span></menu-item>
<menu-item :href="'/servers/certs?type=30days&keyword=' + keyword" :active="type == '30days'">30天过期({{count30Days}})</menu-item>
<span class="item">|</span>
<menu-item :href="'/servers/certs?type=30days&keyword=' + keyword" :active="type == '30days'">30天过期({{count30Days}})</menu-item>
<span class="item disabled">|</span>
<a href="" class="item" @click.prevent="uploadCert">[上传证书]</a>
</second-menu>

Some files were not shown because too many files have changed in this diff Show More