This commit is contained in:
unknown
2026-02-04 20:27:13 +08:00
commit 3b042d1dad
9410 changed files with 1488147 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
package actionutils
import (
"github.com/TeaOSLab/EdgeUser/internal/csrf"
"github.com/iwind/TeaGo/actions"
"net/http"
)
type CSRF struct {
}
func (this *CSRF) BeforeAction(actionPtr actions.ActionWrapper, paramName string) (goNext bool) {
action := actionPtr.Object()
token := action.ParamString("csrfToken")
if !csrf.Validate(token) {
action.ResponseWriter.WriteHeader(http.StatusForbidden)
action.WriteString("表单已失效,请刷新页面后重试(001)")
return
}
return true
}

View File

@@ -0,0 +1,135 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package actionutils
import (
"encoding/json"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/dao"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/oplogs"
"github.com/TeaOSLab/EdgeUser/internal/rpc"
"github.com/TeaOSLab/EdgeUser/internal/utils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/login/loginutils"
"github.com/TeaOSLab/EdgeUser/internal/web/helpers"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
stringutil "github.com/iwind/TeaGo/utils/string"
"github.com/xlzd/gotp"
"time"
)
var TokenSalt = stringutil.Rand(32)
// LoginAction 登录动作
type LoginAction struct {
ParentAction
}
// RunPost 提交
func (this *LoginAction) RunPost(params struct {
Token string
Username string
Password string
OtpCode string
Remember bool
Must *actions.Must
Auth *helpers.UserShouldAuth
CSRF *CSRF
}) {
params.Must.
Field("username", params.Username).
Require("请输入用户名").
Field("password", params.Password).
Require("请输入密码")
if params.Password == stringutil.Md5("") {
this.FailField("password", "请输入密码")
}
// 检查token
if len(params.Token) <= 32 {
this.Fail("请通过登录页面登录")
}
timestampString := params.Token[32:]
if stringutil.Md5(TokenSalt+timestampString) != params.Token[:32] {
this.FailField("refresh", "登录页面已过期,请刷新后重试")
}
timestamp := types.Int64(timestampString)
if timestamp < time.Now().Unix()-1800 {
this.FailField("refresh", "登录页面已过期,请刷新后重试")
}
rpcClient, err := rpc.SharedRPC()
if err != nil {
this.Fail("服务器出了点小问题:" + err.Error())
return
}
resp, err := rpcClient.UserRPC().LoginUser(rpcClient.Context(0), &pb.LoginUserRequest{
Username: params.Username,
Password: params.Password,
})
if err != nil {
err = dao.SharedLogDAO.CreateUserLog(rpcClient.Context(0), oplogs.LevelError, this.Request.URL.Path, "登录时发生系统错误:"+err.Error(), loginutils.RemoteIP(&this.ActionObject))
if err != nil {
utils.PrintError(err)
}
Fail(this, err)
}
if !resp.IsOk {
err = dao.SharedLogDAO.CreateUserLog(rpcClient.Context(0), oplogs.LevelWarn, this.Request.URL.Path, "登录失败,用户名:"+params.Username, loginutils.RemoteIP(&this.ActionObject))
if err != nil {
utils.PrintError(err)
}
this.Fail(resp.Message)
}
// 检查OTP
otpLoginResp, err := this.RPC().LoginRPC().FindEnabledLogin(this.UserContext(), &pb.FindEnabledLoginRequest{
UserId: resp.UserId,
Type: "otp",
})
if err != nil {
this.ErrorPage(err)
return
}
if otpLoginResp.Login != nil && otpLoginResp.Login.IsOn {
loginParams := maps.Map{}
err = json.Unmarshal(otpLoginResp.Login.ParamsJSON, &loginParams)
if err != nil {
this.ErrorPage(err)
return
}
secret := loginParams.GetString("secret")
if gotp.NewDefaultTOTP(secret).Now() != params.OtpCode {
this.Fail("请输入正确的OTP动态密码")
}
}
var userId = resp.UserId
params.Auth.StoreUser(userId, params.Remember)
// 清理老的SESSION
var currentIP = loginutils.RemoteIP(&this.ActionObject)
_, err = this.RPC().LoginSessionRPC().ClearOldLoginSessions(this.UserContext(), &pb.ClearOldLoginSessionsRequest{
Sid: this.Session().Sid,
Ip: currentIP,
})
if err != nil {
this.ErrorPage(err)
return
}
// 记录日志
err = dao.SharedLogDAO.CreateUserLog(rpcClient.Context(userId), oplogs.LevelInfo, this.Request.URL.Path, "成功登录系统,用户名:"+params.Username, currentIP)
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,44 @@
package actionutils
// 子菜单定义
type Menu struct {
Id string `json:"id"`
Name string `json:"name"`
Items []*MenuItem `json:"items"`
IsActive bool `json:"isActive"`
AlwaysActive bool `json:"alwaysActive"`
Index int `json:"index"`
CountNormalItems int `json:"countNormalItems"`
}
// 获取新对象
func NewMenu() *Menu {
return &Menu{
Items: []*MenuItem{},
}
}
// 添加菜单项
func (this *Menu) Add(name string, subName string, url string, isActive bool) *MenuItem {
item := &MenuItem{
Name: name,
SubName: subName,
URL: url,
IsActive: isActive,
}
this.CountNormalItems++
this.Items = append(this.Items, item)
if isActive {
this.IsActive = true
}
return item
}
// 添加特殊菜单项,不计数
func (this *Menu) AddSpecial(name string, subName string, url string, isActive bool) *MenuItem {
item := this.Add(name, subName, url, isActive)
this.CountNormalItems--
return item
}

View File

@@ -0,0 +1,48 @@
package actionutils
import (
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/lists"
)
// 菜单分组
type MenuGroup struct {
Menus []*Menu `json:"menus"`
AlwaysMenu *Menu `json:"alwaysMenu"`
}
// 获取新菜单分组对象
func NewMenuGroup() *MenuGroup {
return &MenuGroup{
Menus: []*Menu{},
}
}
// 查找菜单,如果找不到则自动创建
func (this *MenuGroup) FindMenu(menuId string, menuName string) *Menu {
for _, m := range this.Menus {
if m.Id == menuId {
return m
}
}
menu := NewMenu()
menu.Id = menuId
menu.Name = menuName
menu.Items = []*MenuItem{}
this.Menus = append(this.Menus, menu)
return menu
}
// 排序
func (this *MenuGroup) Sort() {
lists.Sort(this.Menus, func(i int, j int) bool {
menu1 := this.Menus[i]
menu2 := this.Menus[j]
return menu1.Index < menu2.Index
})
}
// 设置子菜单
func SetSubMenu(action actions.ActionWrapper, menu *MenuGroup) {
action.Object().Data["teaSubMenus"] = menu
}

View File

@@ -0,0 +1,14 @@
package actionutils
// 菜单项
type MenuItem struct {
Id string `json:"id"`
Name string `json:"name"`
SubName string `json:"subName"` // 副标题
SupName string `json:"supName"` // 头标
URL string `json:"url"`
IsActive bool `json:"isActive"`
Icon string `json:"icon"`
IsSortable bool `json:"isSortable"`
SubColor string `json:"subColor"`
}

View File

@@ -0,0 +1,161 @@
package actionutils
import (
"fmt"
"github.com/iwind/TeaGo/actions"
"math"
"net/url"
"strings"
)
type Page struct {
Offset int64 // 开始位置
Size int64 // 每页显示数量
Current int64 // 当前页码
Max int64 // 最大页码
Total int64 // 总数量
Path string
Query url.Values
}
func NewActionPage(actionPtr actions.ActionWrapper, total int64, size int64) *Page {
action := actionPtr.Object()
currentPage := action.ParamInt64("page")
paramSize := action.ParamInt64("pageSize")
if paramSize > 0 {
size = paramSize
}
if size <= 0 {
size = 10
}
page := &Page{
Current: currentPage,
Total: total,
Size: size,
Path: action.Request.URL.Path,
Query: action.Request.URL.Query(),
}
page.calculate()
return page
}
func (this *Page) calculate() {
if this.Current < 1 {
this.Current = 1
}
if this.Size <= 0 {
this.Size = 10
}
this.Offset = this.Size * (this.Current - 1)
this.Max = int64(math.Ceil(float64(this.Total) / float64(this.Size)))
}
func (this *Page) AsHTML() string {
if this.Total <= this.Size {
return ""
}
result := []string{}
// 首页
if this.Max > 0 {
result = append(result, `<a href="`+this.composeURL(1)+`">首页</a>`)
} else {
result = append(result, `<a>首页</a>`)
}
// 上一页
if this.Current <= 1 {
result = append(result, `<a>上一页</a>`)
} else {
result = append(result, `<a href="`+this.composeURL(this.Current-1)+`">上一页</a>`)
}
// 中间页数
before5 := this.max(this.Current-5, 1)
after5 := this.min(before5+9, this.Max)
if before5 > 1 {
result = append(result, `<a>...</a>`)
}
for i := before5; i <= after5; i++ {
if i == this.Current {
result = append(result, `<a href="`+this.composeURL(i)+`" class="active">`+fmt.Sprintf("%d", i)+`</a>`)
} else {
result = append(result, `<a href="`+this.composeURL(i)+`">`+fmt.Sprintf("%d", i)+`</a>`)
}
}
if after5 < this.Max {
result = append(result, `<a>...</a>`)
}
// 下一页
if this.Current >= this.Max {
result = append(result, "<a>下一页</a>")
} else {
result = append(result, `<a href="`+this.composeURL(this.Current+1)+`">下一页</a>`)
}
// 尾页
if this.Max > 0 {
result = append(result, `<a href="`+this.composeURL(this.Max)+`">尾页</a>`)
} else {
result = append(result, `<a>尾页</a>`)
}
// 每页数
result = append(result, `<select class="ui dropdown" style="height:34px;padding-top:0;padding-bottom:0;margin-left:1em;color:#666" onchange="ChangePageSize(this.value)">
<option value="10">[每页]</option>`+this.renderSizeOption(10)+
this.renderSizeOption(20)+
this.renderSizeOption(30)+
this.renderSizeOption(40)+
this.renderSizeOption(50)+
this.renderSizeOption(60)+
this.renderSizeOption(70)+
this.renderSizeOption(80)+
this.renderSizeOption(90)+
this.renderSizeOption(100)+`
</select>`)
return `<div class="page">` + strings.Join(result, "") + `</div>`
}
// 判断是否为最后一页
func (this *Page) IsLastPage() bool {
return this.Current == this.Max
}
func (this *Page) composeURL(page int64) string {
this.Query["page"] = []string{fmt.Sprintf("%d", page)}
return this.Path + "?" + this.Query.Encode()
}
func (this *Page) min(i, j int64) int64 {
if i < j {
return i
}
return j
}
func (this *Page) max(i, j int64) int64 {
if i < j {
return j
}
return i
}
func (this *Page) renderSizeOption(size int64) string {
o := `<option value="` + fmt.Sprintf("%d", size) + `"`
if size == this.Size {
o += ` selected="selected"`
}
o += `>` + fmt.Sprintf("%d", size) + `条</option>`
return o
}

View File

@@ -0,0 +1,40 @@
package actionutils
import (
"net/url"
"testing"
)
func TestNewActionPage(t *testing.T) {
page := &Page{
Current: 3,
Total: 105,
Size: 20,
Path: "/hello",
Query: url.Values{
"a": []string{"b"},
"c": []string{"d"},
"page": []string{"3"},
},
}
page.calculate()
t.Log(page.AsHTML())
//logs.PrintAsJSON(page, t)
}
func TestNewActionPage2(t *testing.T) {
page := &Page{
Current: 3,
Total: 105,
Size: 10,
Path: "/hello",
Query: url.Values{
"a": []string{"b"},
"c": []string{"d"},
"page": []string{"3"},
},
}
page.calculate()
t.Log(page.AsHTML())
//logs.PrintAsJSON(page, t)
}

View File

@@ -0,0 +1,280 @@
package actionutils
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
"github.com/TeaOSLab/EdgeCommon/pkg/langs"
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/dao"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
"github.com/TeaOSLab/EdgeUser/internal/configloaders"
"github.com/TeaOSLab/EdgeUser/internal/nodes/serverutils"
"github.com/TeaOSLab/EdgeUser/internal/oplogs"
"github.com/TeaOSLab/EdgeUser/internal/remotelogs"
"github.com/TeaOSLab/EdgeUser/internal/rpc"
"github.com/TeaOSLab/EdgeUser/internal/utils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/login/loginutils"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/lists"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/types"
"net"
"net/http"
"strconv"
)
type ParentAction struct {
actions.ActionObject
rpcClient *rpc.RPCClient
}
// Parent 可以调用自身的一个简便方法
func (this *ParentAction) Parent() *ParentAction {
return this
}
func (this *ParentAction) ErrorPage(err error) {
if err == nil {
return
}
if !rpc.IsConnError(err) {
remotelogs.Error("ERROR_PAGE", this.Request.URL.String()+": "+err.Error())
}
// 日志
this.CreateLog(oplogs.LevelError, codes.UserCommon_LogSystemError, err.Error())
if this.Request.Method == http.MethodGet {
FailPage(this, err)
} else {
Fail(this, err)
}
}
func (this *ParentAction) ErrorText(err string) {
this.ErrorPage(errors.New(err))
}
func (this *ParentAction) NotFound(name string, itemId int64) {
this.ErrorPage(errors.New(name + " id: '" + strconv.FormatInt(itemId, 10) + "' is not found"))
}
func (this *ParentAction) ForbidPage() {
this.ResponseWriter.WriteHeader(http.StatusForbidden)
}
func (this *ParentAction) NewPage(total int64, size ...int64) *Page {
if len(size) > 0 {
return NewActionPage(this, total, size[0])
}
return NewActionPage(this, total, 10)
}
func (this *ParentAction) Nav(mainMenu string, tab string, firstMenu string) {
this.Data["mainMenu"] = mainMenu
this.Data["mainTab"] = tab
this.Data["firstMenuItem"] = firstMenu
}
func (this *ParentAction) FirstMenu(menuItem string) {
this.Data["firstMenuItem"] = menuItem
}
func (this *ParentAction) SecondMenu(menuItem string) {
this.Data["secondMenuItem"] = menuItem
}
func (this *ParentAction) TinyMenu(menuItem string) {
this.Data["tinyMenuItem"] = menuItem
}
func (this *ParentAction) UserId() int64 {
return this.Context.GetInt64("userId")
}
func (this *ParentAction) CreateLog(level string, messageCode langs.MessageCode, args ...any) {
var description = messageCode.For(this.LangCode())
var desc = fmt.Sprintf(description, args...)
if level == oplogs.LevelInfo {
if this.Code != 200 {
level = oplogs.LevelWarn
if len(this.Message) > 0 {
desc += " 失败:" + this.Message
}
}
}
err := dao.SharedLogDAO.CreateUserLog(this.UserContext(), level, this.Request.URL.Path, desc, loginutils.RemoteIP(&this.ActionObject))
if err != nil {
utils.PrintError(err)
}
}
func (this *ParentAction) CreateLogInfo(messageCode langs.MessageCode, args ...any) {
this.CreateLog(oplogs.LevelInfo, messageCode, args...)
}
func (this *ParentAction) LangCode() langs.LangCode {
var langCode = this.Data.GetString("teaLang")
if len(langCode) == 0 {
langCode = langs.DefaultManager().DefaultLang()
}
return langCode
}
// RPC 获取RPC
func (this *ParentAction) RPC() *rpc.RPCClient {
if this.rpcClient != nil {
return this.rpcClient
}
// 所有集群
rpcClient, err := rpc.SharedRPC()
if err != nil {
logs.Fatal(err)
return nil
}
this.rpcClient = rpcClient
return rpcClient
}
// UserContext 获取Context
// 每个请求的context都必须是一个新的实例
func (this *ParentAction) UserContext() context.Context {
if this.rpcClient == nil {
rpcClient, err := rpc.SharedRPC()
if err != nil {
logs.Fatal(err)
return nil
}
this.rpcClient = rpcClient
}
return this.rpcClient.Context(this.UserId())
}
// 一个不包含用户ID的上下文
func (this *ParentAction) NullUserContext() context.Context {
if this.rpcClient == nil {
rpcClient, err := rpc.SharedRPC()
if err != nil {
logs.Fatal(err)
return nil
}
this.rpcClient = rpcClient
}
return this.rpcClient.Context(0)
}
// ValidateFeature 校验Feature
func (this *ParentAction) ValidateFeature(featureCode string, serverId int64) bool {
b, err := this.validateFeature(featureCode, serverId)
if err != nil {
remotelogs.Error("FEATURE", "validate feature: "+err.Error())
}
return b
}
func (this *ParentAction) validateFeature(featureCode string, serverId int64) (bool, error) {
// 检查审核和认证
if configloaders.RequireVerification() && !this.Context.GetBool("isVerified") {
return false, nil
}
if configloaders.RequireIdentity() && !this.Context.GetBool("isIdentified") {
return false, nil
}
var featureDef = userconfigs.FindUserFeature(featureCode)
if featureDef == nil {
return false, errors.New("invalid feature code '" + featureCode + "'")
}
// 检查网站绑定的套餐
if serverId > 0 && featureDef.SupportPlan {
userPlanResp, err := this.RPC().ServerRPC().FindServerUserPlan(this.UserContext(), &pb.FindServerUserPlanRequest{ServerId: serverId})
if err != nil {
return false, err
}
if userPlanResp.UserPlan != nil && userPlanResp.UserPlan.IsOn && userPlanResp.UserPlan.Plan != nil /** 不需要判断套餐是否启用即不需要判断是否为UserPlan.Plan.isOn以便于兼容套餐被删除的情况 **/ {
var plan = userPlanResp.UserPlan.Plan
if plan.HasFullFeatures {
return true, nil
}
var supportedFeatureCodes []string
if len(plan.FeaturesJSON) > 0 {
err = json.Unmarshal(plan.FeaturesJSON, &supportedFeatureCodes)
if err != nil {
return false, err
}
}
return lists.ContainsString(supportedFeatureCodes, featureCode), nil
}
}
// 用户功能
userFeatureResp, err := this.RPC().UserRPC().FindUserFeatures(this.UserContext(), &pb.FindUserFeaturesRequest{UserId: this.UserId()})
if err != nil {
return false, err
}
var userFeatureCodes = []string{}
for _, feature := range userFeatureResp.Features {
userFeatureCodes = append(userFeatureCodes, feature.Code)
}
return lists.ContainsString(userFeatureCodes, featureCode), nil
}
func (this *ParentAction) CheckUserStatus() bool {
// 审核和认证
if configloaders.RequireVerification() && !this.Context.GetBool("isVerified") {
this.RedirectURL("/")
return false
}
if configloaders.RequireIdentity() && !this.Context.GetBool("isIdentified") {
this.RedirectURL("/")
return false
}
return true
}
func (this *ParentAction) FindServerIdWithWebId(webId int64) (serverId int64, err error) {
serverIdResp, err := this.RPC().HTTPWebRPC().FindServerIdWithHTTPWebId(this.UserContext(), &pb.FindServerIdWithHTTPWebIdRequest{
HttpWebId: webId,
})
if err != nil {
this.ErrorPage(err)
return
}
serverId = serverIdResp.ServerId
return
}
func (this *ParentAction) CheckHTTPSRedirecting() {
if this.Request.TLS == nil {
httpsPort, _ := serverutils.ReadServerHTTPS()
if httpsPort > 0 {
currentHost, _, hostErr := net.SplitHostPort(this.Request.Host)
if hostErr != nil {
currentHost = this.Request.Host
}
var newHost = configutils.QuoteIP(currentHost)
if httpsPort != 443 /** default https port **/ {
newHost += ":" + types.String(httpsPort)
}
// 如果没有前端反向代理,则跳转
if len(this.Request.Header.Get("X-Forwarded-For")) == 0 && len(this.Request.Header.Get("X-Real-Ip")) == 0 {
this.RedirectURL("https://" + newHost + this.Request.RequestURI)
return
}
}
}
}

View File

@@ -0,0 +1,59 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package actionutils
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/utils/sizes"
"github.com/iwind/TeaGo/actions"
"io"
)
func (this *ParentAction) UploadFile(formFile *actions.File, businessType string) (fileId int64, ok bool, err error) {
file, err := formFile.OriginFile.Open()
if err != nil {
return 0, false, err
}
fileResp, err := this.RPC().FileRPC().CreateFile(this.UserContext(), &pb.CreateFileRequest{
Filename: formFile.Filename,
Size: formFile.Size,
IsPublic: false,
MimeType: formFile.ContentType,
Type: businessType,
})
if err != nil {
return 0, false, err
}
fileId = fileResp.FileId
var buf = make([]byte, 128*sizes.K) // TODO 使用pool管理
for {
n, err := file.Read(buf)
if n > 0 {
_, chunkErr := this.RPC().FileChunkRPC().CreateFileChunk(this.UserContext(), &pb.CreateFileChunkRequest{
FileId: fileId,
Data: buf[:n],
})
if chunkErr != nil {
return 0, false, chunkErr
}
}
if err != nil {
if err != io.EOF {
return 0, false, nil
}
break
}
}
_, err = this.RPC().FileRPC().UpdateFileFinished(this.UserContext(), &pb.UpdateFileFinishedRequest{
FileId: fileId,
})
if err != nil {
return 0, false, err
}
return fileId, true, nil
}

View File

@@ -0,0 +1,88 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package actionutils
import (
"bytes"
"github.com/TeaOSLab/EdgeUser/internal/configloaders"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
"net/http"
"regexp"
)
var faviconReg = regexp.MustCompile(`<link\s+rel="icon"[^>]+>`)
type PortalAction struct {
ParentAction
}
func (this *PortalAction) IsFrontRequest() bool {
return len(this.Request.URL.Query().Get("X_FROM_FRONT")) > 0
}
func (this *PortalAction) Success() {
if this.IsFrontRequest() {
this.prepareCORSHeader()
}
this.ActionObject.Success()
}
func (this *PortalAction) Show() {
if this.IsFrontRequest() {
this.prepareCORSHeader()
this.ActionObject.Success()
return
}
// add header
this.ViewFunc("X_VIEW_DATA", func() string {
if this.Data == nil {
return ""
}
return string(this.Data.AsJSON())
})
this.ViewFunc("X_VIEW_FAVICON", func() string {
uiConfig, err := configloaders.LoadUIConfig()
if err != nil || uiConfig == nil {
return ""
}
if uiConfig.FaviconFileId > 0 {
return `<link rel="icon" href="/ui/image/` + types.String(uiConfig.FaviconFileId) + `"/>`
}
return ""
})
this.TemplateFilter(func(body []byte) []byte {
// head
body = bytes.ReplaceAll(body, []byte("</head>"), []byte(`
<script>
window.X_FROM_SERVER = true;
window.X_VIEW_DATA = {$ X_VIEW_DATA };
</script>
</head>`))
// favicon
body = faviconReg.ReplaceAll(body, []byte(`{$ X_VIEW_FAVICON }`))
return body
})
this.ViewDir(Tea.Root)
this.ActionObject.Show()
}
func (this *PortalAction) Redirect(url string, status int) {
if this.IsFrontRequest() {
this.Data["X_FRONT_REDIRECT"] = maps.Map{"url": url}
this.Success()
return
}
http.Redirect(this.ResponseWriter, this.Request, url, status)
}
func (this *PortalAction) prepareCORSHeader() {
this.ResponseWriter.Header().Set("Access-Control-Allow-Origin", this.Request.Header.Get("Origin"))
this.ResponseWriter.Header().Set("Access-Control-Allow-Credentials", "true")
}

View File

@@ -0,0 +1,42 @@
package actionutils
import (
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
)
// Tabbar定义
type Tabbar struct {
items []maps.Map
}
// 获取新对象
func NewTabbar() *Tabbar {
return &Tabbar{
items: []maps.Map{},
}
}
// 添加菜单项
func (this *Tabbar) Add(name string, subName string, url string, icon string, active bool) maps.Map {
m := maps.Map{
"name": name,
"subName": subName,
"url": url,
"icon": icon,
"active": active,
"right": false,
}
this.items = append(this.items, m)
return m
}
// 取得所有的Items
func (this *Tabbar) Items() []maps.Map {
return this.items
}
// 设置子菜单
func SetTabbar(action actions.ActionWrapper, tabbar *Tabbar) {
action.Object().Data["teaTabbar"] = tabbar.Items()
}

View File

@@ -0,0 +1,116 @@
package actionutils
import (
"errors"
"fmt"
rpcerrors "github.com/TeaOSLab/EdgeCommon/pkg/rpc/errors"
"github.com/TeaOSLab/EdgeUser/internal/configs"
teaconst "github.com/TeaOSLab/EdgeUser/internal/const"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/logs"
"net/http"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
)
// Fail 提示服务器错误信息
func Fail(action actions.ActionWrapper, err error) {
if err != nil {
logs.Println("[" + reflect.TypeOf(action).String() + "]" + findStack(err.Error()))
}
action.Object().Fail(teaconst.ErrServer + "(请查看日志文件以获取具体信息)")
}
// FailPage 提示页面错误信息
func FailPage(action actions.ActionWrapper, err error) {
if err == nil {
err = errors.New("unknown error")
}
logs.Println("[" + reflect.TypeOf(action).String() + "]" + findStack(err.Error()))
// 当前API终端地址
var apiEndpoints = []string{}
apiConfig, apiConfigErr := configs.LoadAPIConfig()
if apiConfigErr == nil && apiConfig != nil {
apiEndpoints = append(apiEndpoints, apiConfig.RPCEndpoints...)
}
err, _ = rpcerrors.HumanError(err, apiEndpoints, Tea.ConfigFile(configs.ConfigFileName))
action.Object().ResponseWriter.WriteHeader(http.StatusInternalServerError)
if len(action.Object().Request.Header.Get("X-Requested-With")) > 0 {
action.Object().WriteString(teaconst.ErrServer)
} else {
action.Object().WriteString(`<!DOCTYPE html>
<html>
<head>
<title>有系统错误需要处理</title>
<meta charset="UTF-8"/>
<style type="text/css">
hr { border-top: 1px #ccc solid; }
.red { color: red; }
</style>
</head>
<body>
<div style="background: #eee; border: 1px #ccc solid; padding: 10px; font-size: 12px; line-height: 1.8">
` + teaconst.ErrServer + `
<div>可以通过查看日志文件查看具体的错误提示。</div>
<hr/>
<div class="red">Error: ` + err.Error() + `</div>
</div>
</body>
</html>`)
}
}
// MatchPath 判断动作的文件路径是否相当
func MatchPath(action *actions.ActionObject, path string) bool {
return action.Request.URL.Path == path
}
// FindParentAction 查找父级Action
func FindParentAction(actionPtr actions.ActionWrapper) *ParentAction {
parentActionValue, ok := actionPtr.(interface {
Parent() *ParentAction
})
if ok {
return parentActionValue.Parent()
}
return nil
}
func findStack(err string) string {
_, currentFilename, _, currentOk := runtime.Caller(1)
if currentOk {
for i := 1; i < 32; i++ {
_, filename, lineNo, ok := runtime.Caller(i)
if !ok {
break
}
if filename == currentFilename || filepath.Base(filename) == "parent_action.go" {
continue
}
goPath := os.Getenv("GOPATH")
if len(goPath) > 0 {
absGoPath, err := filepath.Abs(goPath)
if err == nil {
filename = strings.TrimPrefix(filename, absGoPath)[1:]
}
} else if strings.Contains(filename, "src") {
filename = filename[strings.Index(filename, "src"):]
}
err += "\n\t\t" + filename + ":" + fmt.Sprintf("%d", lineNo)
break
}
}
return err
}

View File

@@ -0,0 +1,12 @@
package account
import "github.com/iwind/TeaGo"
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Prefix("/account").
GetPost("/reset", new(ResetAction)).
EndAll()
})
}

View File

@@ -0,0 +1,161 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package account
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/configloaders"
teaconst "github.com/TeaOSLab/EdgeUser/internal/const"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/actions"
"regexp"
"strings"
)
type ResetAction struct {
actionutils.ParentAction
}
func (this *ResetAction) Init() {
this.Nav("", "", "")
}
func (this *ResetAction) RunGet(params struct{}) {
// 界面
config, err := configloaders.LoadUIConfig()
if err != nil {
this.ErrorPage(err)
return
}
this.Data["systemName"] = config.UserSystemName
this.Data["showVersion"] = config.ShowVersion
if len(config.Version) > 0 {
this.Data["version"] = config.Version
} else {
this.Data["version"] = teaconst.Version
}
this.Data["faviconFileId"] = config.FaviconFileId
// 密码强度
registerConfig, err := configloaders.LoadRegisterConfig()
if err != nil {
this.Fail("暂未开放注册功能")
}
this.Data["complexPassword"] = registerConfig.ComplexPassword
this.Show()
}
func (this *ResetAction) RunPost(params struct {
Action string
UsernameOrEmail string
Code string
NewPass string
NewPass2 string
Must *actions.Must
CSRF *actionutils.CSRF
}) {
// 检查用户名或邮箱是否存在
if len(params.UsernameOrEmail) == 0 {
this.FailField("usernameOrEmail", "请输入用户名或已经绑定的邮箱")
return
}
var email string
if strings.Contains(params.UsernameOrEmail, "@") { // 邮箱
checkResp, err := this.RPC().UserRPC().CheckUserEmail(this.UserContext(), &pb.CheckUserEmailRequest{Email: params.UsernameOrEmail})
if err != nil {
this.ErrorPage(err)
return
}
if !checkResp.Exists {
this.FailField("usernameOrEmail", "找不到和此邮箱绑定的用户")
return
}
email = params.UsernameOrEmail
} else { // 用户名
checkResp, err := this.RPC().UserRPC().CheckUserUsername(this.UserContext(), &pb.CheckUserUsernameRequest{
UserId: 0,
Username: params.UsernameOrEmail,
})
if err != nil {
this.ErrorPage(err)
return
}
if !checkResp.Exists {
this.FailField("usernameOrEmail", "此用户名尚未注册")
return
}
// 检查是否已绑定邮箱
emailResp, err := this.RPC().UserRPC().FindUserVerifiedEmailWithUsername(this.UserContext(), &pb.FindUserVerifiedEmailWithUsernameRequest{Username: params.UsernameOrEmail})
if err != nil {
this.ErrorPage(err)
return
}
if len(emailResp.Email) == 0 {
this.FailField("usernameOrEmail", "此用户尚未绑定邮箱,不能通过邮箱找回密码")
return
}
email = emailResp.Email
}
// 发送验证码
if params.Action == "send" {
_, err := this.RPC().UserVerifyCodeRPC().SendUserVerifyCode(this.UserContext(), &pb.SendUserVerifyCodeRequest{
Type: "resetPassword",
Email: email,
Mobile: "", // TODO 将来实现通过手机号找回
})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["email"] = email
}
// 修改密码
if params.Action == "update" {
// 密码
params.Must.
Field("newPass", params.NewPass).
Require("请输入新登录密码").
MinLength(6, "登录密码长度不能小于6位")
registerConfig, err := configloaders.LoadRegisterConfig()
if err == nil {
if registerConfig.ComplexPassword && (!regexp.MustCompile(`[a-z]`).MatchString(params.NewPass) || !regexp.MustCompile(`[A-Z]`).MatchString(params.NewPass)) {
this.FailField("newPass", "为了您的账号安全,密码中必须包含大写和小写字母")
return
}
}
if params.NewPass != params.NewPass2 {
this.FailField("newPass2", "两次输入的密码不一致")
return
}
// 校验验证码
validateResp, err := this.RPC().UserVerifyCodeRPC().ValidateUserVerifyCode(this.UserContext(), &pb.ValidateUserVerifyCodeRequest{
Type: "resetPassword",
Email: email,
Mobile: "", // TODO 将来实现
Code: params.Code,
NewPassword: params.NewPass,
})
if err != nil {
this.ErrorPage(err)
return
}
if !validateResp.IsOk {
this.Fail("验证失败:" + validateResp.ErrorMessage)
return
}
}
this.Success()
}

View File

@@ -0,0 +1,44 @@
package accesskeys
import (
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/actions"
)
type CreatePopupAction struct {
actionutils.ParentAction
}
func (this *CreatePopupAction) Init() {
this.Nav("", "", "")
}
func (this *CreatePopupAction) RunGet(params struct{}) {
this.Show()
}
func (this *CreatePopupAction) RunPost(params struct {
Description string
Must *actions.Must
CSRF *actionutils.CSRF
}) {
params.Must.
Field("description", params.Description).
Require("请输入备注")
accessKeyIdResp, err := this.RPC().UserAccessKeyRPC().CreateUserAccessKey(this.UserContext(), &pb.CreateUserAccessKeyRequest{
UserId: this.UserId(),
Description: params.Description,
})
if err != nil {
this.ErrorPage(err)
return
}
defer this.CreateLogInfo(codes.UserAccessKey_LogCreateUserAccessKey, accessKeyIdResp.UserAccessKeyId)
this.Success()
}

View File

@@ -0,0 +1,25 @@
package accesskeys
import (
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
)
type DeleteAction struct {
actionutils.ParentAction
}
func (this *DeleteAction) RunPost(params struct {
AccessKeyId int64
}) {
defer this.CreateLogInfo(codes.UserAccessKey_LogDeleteUserAccessKey, params.AccessKeyId)
_, err := this.RPC().UserAccessKeyRPC().DeleteUserAccessKey(this.UserContext(), &pb.DeleteUserAccessKeyRequest{UserAccessKeyId: params.AccessKeyId})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,37 @@
package accesskeys
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "", "")
}
func (this *IndexAction) RunGet(params struct{}) {
accessKeysResp, err := this.RPC().UserAccessKeyRPC().FindAllEnabledUserAccessKeys(this.UserContext(), &pb.FindAllEnabledUserAccessKeysRequest{UserId: this.UserId()})
if err != nil {
this.ErrorPage(err)
return
}
accessKeyMaps := []maps.Map{}
for _, accessKey := range accessKeysResp.UserAccessKeys {
accessKeyMaps = append(accessKeyMaps, maps.Map{
"id": accessKey.Id,
"isOn": accessKey.IsOn,
"uniqueId": accessKey.UniqueId,
"secret": accessKey.Secret,
"description": accessKey.Description,
})
}
this.Data["accessKeys"] = accessKeyMaps
this.Show()
}

View File

@@ -0,0 +1,20 @@
package accesskeys
import (
"github.com/TeaOSLab/EdgeUser/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth("")).
Data("teaMenu", "acl").
Prefix("/acl/accesskeys").
Get("", new(IndexAction)).
GetPost("/createPopup", new(CreatePopupAction)).
Post("/delete", new(DeleteAction)).
Post("/updateIsOn", new(UpdateIsOnAction)).
EndAll()
})
}

View File

@@ -0,0 +1,29 @@
package accesskeys
import (
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
)
type UpdateIsOnAction struct {
actionutils.ParentAction
}
func (this *UpdateIsOnAction) RunPost(params struct {
AccessKeyId int64
IsOn bool
}) {
defer this.CreateLogInfo(codes.UserAccessKey_LogUpdateUserAccessKeyIsOn, params.AccessKeyId)
_, err := this.RPC().UserAccessKeyRPC().UpdateUserAccessKeyIsOn(this.UserContext(), &pb.UpdateUserAccessKeyIsOnRequest{
UserAccessKeyId: params.AccessKeyId,
IsOn: params.IsOn,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,17 @@
package acl
import (
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "", "")
}
func (this *IndexAction) RunGet(params struct{}) {
this.RedirectURL("/acl/accesskeys")
}

View File

@@ -0,0 +1,17 @@
package acl
import (
"github.com/TeaOSLab/EdgeUser/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth("")).
Data("teaMenu", "acl").
Prefix("/acl").
Get("", new(IndexAction)).
EndAll()
})
}

View File

@@ -0,0 +1,17 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package antiddos
import "github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "", "")
}
func (this *IndexAction) RunGet(params struct{}) {
this.RedirectURL("/anti-ddos/instances")
}

View File

@@ -0,0 +1,40 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package antiddos
import (
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/anti-ddos/instances"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/anti-ddos/packages"
"github.com/TeaOSLab/EdgeUser/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth("")).
Data("teaMenu", "anti-ddos").
Prefix("/anti-ddos").
Get("", new(IndexAction)).
// 实例
Prefix("/anti-ddos/instances").
Data("teaSubMenu", "instance").
Get("", new(instances.IndexAction)).
Post("/delete", new(instances.DeleteAction)).
GetPost("/updateObjectsPopup", new(instances.UpdateObjectsPopupAction)).
Post("/userServers", new(instances.UserServersAction)).
GetPost("/renew", new(instances.RenewAction)).
GetPost("/renewConfirm", new(instances.RenewConfirmAction)).
// 产品
Prefix("/anti-ddos/packages").
Data("teaSubMenu", "package").
Get("", new(packages.IndexAction)).
Post("/price", new(packages.PriceAction)).
GetPost("/confirm", new(packages.ConfirmAction)).
//
EndAll()
})
}

View File

@@ -0,0 +1,27 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package instances
import (
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
)
type DeleteAction struct {
actionutils.ParentAction
}
func (this *DeleteAction) RunPost(params struct {
UserInstanceId int64
}) {
defer this.CreateLogInfo(codes.UserADInstance_LogDeleteUserADInstance, params.UserInstanceId)
_, err := this.RPC().UserADInstanceRPC().DeleteUserADInstance(this.UserContext(), &pb.DeleteUserADInstanceRequest{UserADInstanceId: params.UserInstanceId})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,102 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package instances
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
"github.com/TeaOSLab/EdgeUser/internal/utils/dateutils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
timeutil "github.com/iwind/TeaGo/utils/time"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "", "")
}
func (this *IndexAction) RunGet(params struct {
NetworkId int64
PeriodId int64
}) {
countResp, err := this.RPC().UserADInstanceRPC().CountUserADInstances(this.UserContext(), &pb.CountUserADInstancesRequest{
AdNetworkId: params.NetworkId,
ExpiresDay: "",
AdPackagePeriodId: params.PeriodId,
})
if err != nil {
this.ErrorPage(err)
return
}
var count = countResp.Count
var page = this.NewPage(count)
this.Data["page"] = page.AsHTML()
userInstancesResp, err := this.RPC().UserADInstanceRPC().ListUserADInstances(this.UserContext(), &pb.ListUserADInstancesRequest{
AdNetworkId: params.NetworkId,
AdPackagePeriodId: params.PeriodId,
ExpiresDay: "",
AvailableOnly: false,
Offset: page.Offset,
Size: page.Size,
})
if err != nil {
this.ErrorPage(err)
return
}
var userInstanceMaps = []maps.Map{}
for _, userInstance := range userInstancesResp.UserADInstances {
// 实例
var instanceMap = maps.Map{
"id": 0,
"userInstanceId": 0,
}
if userInstance.AdPackageInstance != nil {
instanceMap = maps.Map{
"id": userInstance.AdPackageInstance.Id,
"userInstanceId": userInstance.AdPackageInstance.UserInstanceId,
}
}
// 产品
var packageMap = maps.Map{
"id": 0,
}
if userInstance.AdPackageInstance != nil && userInstance.AdPackageInstance.AdPackage != nil {
packageMap = maps.Map{
"id": userInstance.AdPackageInstance.AdPackage.Id,
"summary": userInstance.AdPackageInstance.AdPackage.Summary,
}
}
// 实例
var ipAddresses = []string{}
if userInstance.AdPackageInstance != nil && len(userInstance.AdPackageInstance.IpAddresses) > 0 {
ipAddresses = userInstance.AdPackageInstance.IpAddresses
}
userInstanceMaps = append(userInstanceMaps, maps.Map{
"id": userInstance.Id,
"dayFrom": dateutils.SplitYmd(userInstance.DayFrom),
"dayTo": dateutils.SplitYmd(userInstance.DayTo),
"instance": instanceMap,
"package": packageMap,
"ipAddresses": ipAddresses,
"createdTime": timeutil.FormatTime("Y-m-d H:i:s", userInstance.CreatedAt),
"periodCount": userInstance.AdPackagePeriodCount,
"periodUnitName": userconfigs.ADPackagePeriodUnitName(userInstance.AdPackagePeriodUnit),
"canDelete": true,
"isExpired": userInstance.DayTo < timeutil.Format("Ymd"),
"isAvailable": userInstance.IsAvailable,
"countObjects": userInstance.CountObjects,
})
}
this.Data["userInstances"] = userInstanceMaps
this.Show()
}

View File

@@ -0,0 +1,114 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package instances
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
"github.com/TeaOSLab/EdgeUser/internal/utils/dateutils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/lists"
"github.com/iwind/TeaGo/maps"
timeutil "github.com/iwind/TeaGo/utils/time"
)
type RenewAction struct {
actionutils.ParentAction
}
func (this *RenewAction) RunGet(params struct {
UserInstanceId int64
}) {
userInstanceResp, err := this.RPC().UserADInstanceRPC().FindUserADInstance(this.UserContext(), &pb.FindUserADInstanceRequest{UserADInstanceId: params.UserInstanceId})
if err != nil {
this.ErrorPage(err)
return
}
var userInstance = userInstanceResp.UserADInstance
if userInstance == nil {
this.NotFound("userInstance", params.UserInstanceId)
return
}
// 实例
var instanceMap = maps.Map{
"id": 0,
}
if userInstance.AdPackageInstance != nil {
if userInstance.AdPackageInstance.IpAddresses == nil {
userInstance.AdPackageInstance.IpAddresses = []string{}
}
instanceMap = maps.Map{
"id": userInstance.AdPackageInstance.Id,
"ipAddresses": userInstance.AdPackageInstance.IpAddresses,
}
}
// 产品
var packageMap = maps.Map{
"id": 0,
}
var packageId int64
if userInstance.AdPackageInstance != nil && userInstance.AdPackageInstance.AdPackage != nil {
packageId = userInstance.AdPackageInstance.AdPackage.Id
packageMap = maps.Map{
"id": userInstance.AdPackageInstance.AdPackage.Id,
"summary": userInstance.AdPackageInstance.AdPackage.Summary,
}
}
this.Data["userInstance"] = maps.Map{
"id": userInstance.Id,
"periodId": userInstance.AdPackagePeriodId,
"dayTo": dateutils.SplitYmd(userInstance.DayTo),
"today": timeutil.Format("Y-m-d"),
"isExpired": len(userInstance.DayTo) == 0 || userInstance.DayTo < timeutil.Format("Ymd"),
"isAvailable": userInstance.IsAvailable,
"instance": instanceMap,
"package": packageMap,
}
// 价格选项
if packageId > 0 {
pricesResp, err := this.RPC().ADPackagePriceRPC().FindADPackagePrices(this.UserContext(), &pb.FindADPackagePricesRequest{AdPackageId: packageId})
if err != nil {
this.ErrorPage(err)
return
}
var priceMaps = []maps.Map{}
var allValidPeriodIds = []int64{}
for _, price := range pricesResp.AdPackagePrices {
allValidPeriodIds = append(allValidPeriodIds, price.AdPackagePeriodId)
priceMaps = append(priceMaps, maps.Map{
"periodId": price.AdPackagePeriodId,
"price": price.Price,
})
}
this.Data["prices"] = priceMaps
// 有效期选项
var periodMaps = []maps.Map{}
periodResp, err := this.RPC().ADPackagePeriodRPC().FindAllAvailableADPackagePeriods(this.UserContext(), &pb.FindAllAvailableADPackagePeriodsRequest{})
if err != nil {
this.ErrorPage(err)
return
}
for _, period := range periodResp.AdPackagePeriods {
if !lists.ContainsInt64(allValidPeriodIds, period.Id) {
continue
}
periodMaps = append(periodMaps, maps.Map{
"id": period.Id,
"count": period.Count,
"unit": period.Unit,
"unitName": userconfigs.ADPackagePeriodUnitName(period.Unit),
})
}
this.Data["allPeriods"] = periodMaps
} else {
this.Data["allPeriods"] = []maps.Map{}
this.Data["prices"] = []maps.Map{}
}
this.Show()
}

View File

@@ -0,0 +1,204 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package instances
import (
"encoding/json"
"fmt"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
"github.com/TeaOSLab/EdgeUser/internal/utils/dateutils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/finance/financeutils"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/types"
)
type RenewConfirmAction struct {
actionutils.ParentAction
}
func (this *RenewConfirmAction) Init() {
this.Nav("", "", "")
}
func (this *RenewConfirmAction) RunGet(params struct {
UserInstanceId int64
PeriodId int64
}) {
this.Data["userInstanceId"] = params.UserInstanceId
this.Data["periodId"] = params.PeriodId
periodResp, err := this.RPC().ADPackagePeriodRPC().FindADPackagePeriod(this.UserContext(), &pb.FindADPackagePeriodRequest{AdPackagePeriodId: params.PeriodId})
if err != nil {
this.ErrorPage(err)
return
}
var period = periodResp.AdPackagePeriod
if period == nil {
this.ErrorText("period not found")
return
}
this.Data["periodName"] = types.String(period.Count) + userconfigs.ADPackagePeriodUnitName(period.Unit)
userInstanceResp, err := this.RPC().UserADInstanceRPC().FindUserADInstance(this.UserContext(), &pb.FindUserADInstanceRequest{UserADInstanceId: params.UserInstanceId})
if err != nil {
this.ErrorPage(err)
return
}
var userInstance = userInstanceResp.UserADInstance
if userInstance == nil {
this.NotFound("userInstance", params.UserInstanceId)
return
}
this.Data["dayTo"] = dateutils.SplitYmd(userInstance.DayTo)
// 实例
if userInstance.AdPackageInstance == nil {
this.ErrorText("adPackageInstance not found")
return
}
if userInstance.AdPackageInstance.IpAddresses == nil {
userInstance.AdPackageInstance.IpAddresses = []string{}
}
this.Data["ipAddresses"] = userInstance.AdPackageInstance.IpAddresses
// 产品
if userInstance.AdPackageInstance.AdPackage == nil {
this.ErrorText("adPackage not found")
return
}
var packageId = userInstance.AdPackageInstance.AdPackage.Id
this.Data["packageSummary"] = userInstance.AdPackageInstance.AdPackage.Summary
// 价格
priceResp, err := this.RPC().ADPackagePriceRPC().FindADPackagePrice(this.UserContext(), &pb.FindADPackagePriceRequest{
AdPackageId: packageId,
AdPackagePeriodId: params.PeriodId,
Count: 1,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["amount"] = priceResp.Amount
this.Show()
}
func (this *RenewConfirmAction) RunPost(params struct {
UserInstanceId int64
PeriodId int64
MethodCode string
Must *actions.Must
CSRF *actionutils.CSRF
}) {
if params.UserInstanceId <= 0 || params.PeriodId <= 0 {
this.Fail("参数错误,请重新下单")
return
}
userInstanceResp, err := this.RPC().UserADInstanceRPC().FindUserADInstance(this.UserContext(), &pb.FindUserADInstanceRequest{UserADInstanceId: params.UserInstanceId})
if err != nil {
this.ErrorPage(err)
return
}
var userInstance = userInstanceResp.UserADInstance
if userInstance == nil {
this.NotFound("userInstance", params.UserInstanceId)
return
}
if userInstance.AdPackageInstance == nil {
this.ErrorText("invalid adPackageInstance")
return
}
var packageId = userInstance.AdPackageInstance.AdPackageId
if !userInstance.IsAvailable {
this.ErrorText("invalid user instance")
return
}
periodResp, err := this.RPC().ADPackagePeriodRPC().FindADPackagePeriod(this.UserContext(), &pb.FindADPackagePeriodRequest{AdPackagePeriodId: params.PeriodId})
if err != nil {
this.ErrorPage(err)
return
}
var period = periodResp.AdPackagePeriod
if period == nil {
this.Fail("invalid 'periodId'")
return
}
priceResp, err := this.RPC().ADPackagePriceRPC().FindADPackagePrice(this.UserContext(), &pb.FindADPackagePriceRequest{
AdPackageId: packageId,
AdPackagePeriodId: params.PeriodId,
Count: 1,
})
if err != nil {
this.ErrorPage(err)
return
}
var amount = priceResp.Amount
if amount <= 0 {
this.Fail("invalid 'amount': " + types.String(amount))
return
}
// 使用余额购买
this.Data["success"] = false
this.Data["orderCode"] = ""
if params.MethodCode == "@balance" {
balance, err := financeutils.FindUserBalance(this.UserContext())
if err != nil {
this.ErrorPage(err)
return
}
if amount > balance {
this.Fail("当前余额不足,需要:" + fmt.Sprintf("%.2f元", amount) + ",现有:" + fmt.Sprintf("%.2f元", balance) + " ,请充值后再试")
return
}
// 直接购买
_, err = this.RPC().UserADInstanceRPC().RenewUserADInstance(this.UserContext(), &pb.RenewUserADInstanceRequest{
UserADInstanceId: params.UserInstanceId,
AdPackagePeriodId: params.PeriodId,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["success"] = true
this.Success()
return
}
// 生成订单
var orderParams = &userconfigs.OrderTypeRenewAntiDDoSInstanceParams{
UserInstanceId: params.UserInstanceId,
PeriodId: params.PeriodId,
}
orderParamsJSON, err := json.Marshal(orderParams)
if err != nil {
this.ErrorPage(err)
return
}
resp, err := this.RPC().UserOrderRPC().CreateUserOrder(this.UserContext(), &pb.CreateUserOrderRequest{
Type: userconfigs.OrderTypeRenewAntiDDoSInstance,
OrderMethodCode: params.MethodCode,
Amount: amount,
ParamsJSON: orderParamsJSON,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["orderCode"] = resp.Code
this.Success()
}

View File

@@ -0,0 +1,129 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package instances
import (
"encoding/json"
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/utils/dateutils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
timeutil "github.com/iwind/TeaGo/utils/time"
)
type UpdateObjectsPopupAction struct {
actionutils.ParentAction
}
func (this *UpdateObjectsPopupAction) Init() {
this.Nav("", "", "")
}
func (this *UpdateObjectsPopupAction) RunGet(params struct {
UserInstanceId int64
}) {
userInstanceResp, err := this.RPC().UserADInstanceRPC().FindUserADInstance(this.UserContext(), &pb.FindUserADInstanceRequest{UserADInstanceId: params.UserInstanceId})
if err != nil {
this.ErrorPage(err)
return
}
var userInstance = userInstanceResp.UserADInstance
if userInstance == nil {
this.NotFound("userInstance", params.UserInstanceId)
return
}
var objectMaps = []maps.Map{}
if len(userInstance.ObjectsJSON) > 0 {
err = json.Unmarshal(userInstance.ObjectsJSON, &objectMaps)
if err != nil {
this.ErrorPage(err)
return
}
}
// 用户
var userMap = maps.Map{
"id": 0,
}
if userInstance.User != nil {
userMap = maps.Map{
"id": userInstance.User.Id,
"fullname": userInstance.User.Fullname,
"username": userInstance.User.Username,
}
}
// 实例
var instanceMap = maps.Map{
"id": 0,
}
if userInstance.AdPackageInstance != nil {
if userInstance.AdPackageInstance.IpAddresses == nil {
userInstance.AdPackageInstance.IpAddresses = []string{}
}
instanceMap = maps.Map{
"id": userInstance.AdPackageInstance.Id,
"ipAddresses": userInstance.AdPackageInstance.IpAddresses,
}
}
// 产品
var packageMap = maps.Map{
"id": 0,
}
if userInstance.AdPackageInstance != nil && userInstance.AdPackageInstance.AdPackage != nil {
packageMap = maps.Map{
"id": userInstance.AdPackageInstance.AdPackage.Id,
"summary": userInstance.AdPackageInstance.AdPackage.Summary,
}
}
this.Data["userInstance"] = maps.Map{
"id": userInstance.Id,
"periodId": userInstance.AdPackagePeriodId,
"isAvailable": userInstance.IsAvailable,
"objects": objectMaps,
"dayTo": dateutils.SplitYmd(userInstance.DayTo),
"today": timeutil.Format("Y-m-d"),
"isExpired": len(userInstance.DayTo) == 0 || userInstance.DayTo < timeutil.Format("Ymd"),
"user": userMap,
"instance": instanceMap,
"package": packageMap,
}
this.Show()
}
func (this *UpdateObjectsPopupAction) RunPost(params struct {
UserInstanceId int64
ObjectCodesJSON []byte
Must *actions.Must
CSRF *actionutils.CSRF
}) {
defer this.CreateLogInfo(codes.UserADInstance_LogUpdateUserADInstanceObjects, params.UserInstanceId)
var objectCodes = []string{}
if len(params.ObjectCodesJSON) > 0 {
err := json.Unmarshal(params.ObjectCodesJSON, &objectCodes)
if err != nil {
this.ErrorPage(err)
return
}
}
// TODO 检查有没有超出最大防护对象数量
_, err := this.RPC().UserADInstanceRPC().UpdateUserADInstanceObjects(this.UserContext(), &pb.UpdateUserADInstanceObjectsRequest{
UserADInstanceId: params.UserInstanceId,
ObjectCodes: objectCodes,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,57 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package instances
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
)
type UserServersAction struct {
actionutils.ParentAction
}
func (this *UserServersAction) RunPost(params struct {
Page int32
}) {
var size int64 = 10
// 数量
countResp, err := this.RPC().ServerRPC().CountAllEnabledServersMatch(this.UserContext(), &pb.CountAllEnabledServersMatchRequest{
UserId: this.UserId(),
})
if err != nil {
this.ErrorPage(err)
return
}
var count = countResp.Count
var page = this.NewPage(count, size)
// 列表
serversResp, err := this.RPC().ServerRPC().ListEnabledServersMatch(this.UserContext(), &pb.ListEnabledServersMatchRequest{
Offset: page.Offset,
Size: page.Size,
UserId: this.UserId(),
IgnoreServerNames: true,
IgnoreSSLCerts: true,
})
if err != nil {
this.ErrorPage(err)
return
}
var serverMaps = []maps.Map{}
for _, server := range serversResp.Servers {
serverMaps = append(serverMaps, maps.Map{
"id": server.Id,
"name": server.Name,
})
}
this.Data["servers"] = serverMaps
this.Data["page"] = maps.Map{
"max": page.Max,
}
this.Success()
}

View File

@@ -0,0 +1,190 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package packages
import (
"encoding/json"
"fmt"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/finance/financeutils"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/types"
)
type ConfirmAction struct {
actionutils.ParentAction
}
func (this *ConfirmAction) Init() {
this.Nav("", "", "")
}
func (this *ConfirmAction) RunGet(params struct {
PackageId int64
PeriodId int64
Count int32
}) {
this.Data["packageId"] = params.PackageId
this.Data["periodId"] = params.PeriodId
this.Data["count"] = params.Count
this.Data["packageSummary"] = ""
this.Data["periodName"] = ""
this.Data["amount"] = 0
if params.PackageId <= 0 || params.PeriodId <= 0 || params.Count <= 0 {
this.Show()
return
}
// 产品信息
packageResp, err := this.RPC().ADPackageRPC().FindADPackage(this.UserContext(), &pb.FindADPackageRequest{AdPackageId: params.PackageId})
if err != nil {
this.ErrorPage(err)
return
}
var adPackage = packageResp.AdPackage
if adPackage == nil {
this.NotFound("adPackage", params.PackageId)
return
}
this.Data["packageSummary"] = adPackage.Summary
// 日期信息
periodResp, err := this.RPC().ADPackagePeriodRPC().FindADPackagePeriod(this.UserContext(), &pb.FindADPackagePeriodRequest{AdPackagePeriodId: params.PeriodId})
if err != nil {
this.ErrorPage(err)
return
}
var period = periodResp.AdPackagePeriod
if period == nil {
this.NotFound("adPackagePeriod", params.PeriodId)
return
}
this.Data["periodName"] = types.String(period.Count) + userconfigs.ADPackagePeriodUnitName(period.Unit)
// 数量
countInstancesResp, err := this.RPC().ADPackageInstanceRPC().CountIdleADPackageInstances(this.UserContext(), &pb.CountIdleADPackageInstancesRequest{AdPackageId: params.PackageId})
if err != nil {
this.ErrorPage(err)
return
}
var countInstances = types.Int32(countInstancesResp.Count)
if countInstances < params.Count {
this.Show()
return
}
resp, err := this.RPC().ADPackagePriceRPC().FindADPackagePrice(this.UserContext(), &pb.FindADPackagePriceRequest{
AdPackageId: params.PackageId,
AdPackagePeriodId: params.PeriodId,
Count: params.Count,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["amount"] = resp.Amount
this.Show()
}
func (this *ConfirmAction) RunPost(params struct {
PackageId int64
PeriodId int64
Count int32
MethodCode string
Must *actions.Must
CSRF *actionutils.CSRF
}) {
if params.PackageId <= 0 || params.PeriodId <= 0 || params.Count <= 0 {
this.Fail("参数错误,请重新下单")
return
}
periodResp, err := this.RPC().ADPackagePeriodRPC().FindADPackagePeriod(this.UserContext(), &pb.FindADPackagePeriodRequest{AdPackagePeriodId: params.PeriodId})
if err != nil {
this.ErrorPage(err)
return
}
var period = periodResp.AdPackagePeriod
if period == nil {
this.Fail("invalid 'periodId'")
return
}
priceResp, err := this.RPC().ADPackagePriceRPC().FindADPackagePrice(this.UserContext(), &pb.FindADPackagePriceRequest{
AdPackageId: params.PackageId,
AdPackagePeriodId: params.PeriodId,
Count: params.Count,
})
if err != nil {
this.ErrorPage(err)
return
}
var amount = priceResp.Amount
// 使用余额购买
this.Data["success"] = false
this.Data["orderCode"] = ""
if params.MethodCode == "@balance" {
balance, err := financeutils.FindUserBalance(this.UserContext())
if err != nil {
this.ErrorPage(err)
return
}
if amount > balance {
this.Fail("当前余额不足,需要:" + fmt.Sprintf("%.2f元", amount) + ",现有:" + fmt.Sprintf("%.2f元", balance) + " ,请充值后再试")
return
}
// 直接购买
_, err = this.RPC().UserADInstanceRPC().BuyUserADInstance(this.UserContext(), &pb.BuyUserADInstanceRequest{
UserId: this.UserId(),
AdPackageId: params.PackageId,
AdPackagePeriodId: params.PeriodId,
Count: params.Count,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["success"] = true
this.Success()
return
}
// 生成订单
var orderParams = &userconfigs.OrderTypeBuyAntiDDoSInstanceParams{
PackageId: params.PackageId,
PeriodId: params.PeriodId,
PeriodCount: period.Count,
PeriodUnit: period.Unit,
Count: params.Count,
}
orderParamsJSON, err := json.Marshal(orderParams)
if err != nil {
this.ErrorPage(err)
return
}
resp, err := this.RPC().UserOrderRPC().CreateUserOrder(this.UserContext(), &pb.CreateUserOrderRequest{
Type: userconfigs.OrderTypeBuyAntiDDoSInstance,
OrderMethodCode: params.MethodCode,
Amount: amount,
ParamsJSON: orderParamsJSON,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["orderCode"] = resp.Code
this.Success()
}

View File

@@ -0,0 +1,150 @@
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package packages
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/lists"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "", "")
}
func (this *IndexAction) RunGet(params struct{}) {
// 高防产品
var allPackageMap = map[int64]*pb.ADPackage{} // packageId => *pb.ADPackage
packagesResp, err := this.RPC().ADPackageRPC().FindAllIdleADPackages(this.UserContext(), &pb.FindAllIdleADPackagesRequest{})
if err != nil {
this.ErrorPage(err)
return
}
for _, p := range packagesResp.AdPackages {
allPackageMap[p.Id] = p
}
// 价格
pricesResp, err := this.RPC().ADPackagePriceRPC().FindAllADPackagePrices(this.UserContext(), &pb.FindAllADPackagePricesRequest{})
if err != nil {
this.ErrorPage(err)
return
}
var priceMaps = []maps.Map{}
for _, price := range pricesResp.AdPackagePrices {
if price.Price > 0 {
var packageId = price.AdPackageId
var periodId = price.AdPackagePeriodId
p, ok := allPackageMap[packageId]
if !ok {
continue
}
var networkId = p.AdNetworkId
var countIdleInstances = p.CountIdleADPackageInstances
if packageId > 0 && periodId > 0 && networkId > 0 && countIdleInstances > 0 {
priceMaps = append(priceMaps, maps.Map{
"networkId": networkId,
"packageId": packageId,
"protectionBandwidth": types.String(p.ProtectionBandwidthSize) + userconfigs.ADPackageSizeFullUnit(p.ProtectionBandwidthUnit),
"serverBandwidth": types.String(p.ServerBandwidthSize) + userconfigs.ADPackageSizeFullUnit(p.ServerBandwidthUnit),
"periodId": periodId,
"price": price.Price,
"maxInstances": countIdleInstances,
})
}
}
}
this.Data["prices"] = priceMaps
// 重新处理防护带宽和服务带宽
var allProtectionBandwidthSizes = []string{}
var allServerBandwidthSizes = []string{}
for _, p := range packagesResp.AdPackages {
var protectionSize = types.String(p.ProtectionBandwidthSize) + userconfigs.ADPackageSizeFullUnit(p.ProtectionBandwidthUnit)
var serverSize = types.String(p.ServerBandwidthSize) + userconfigs.ADPackageSizeFullUnit(p.ServerBandwidthUnit)
if !lists.ContainsString(allProtectionBandwidthSizes, protectionSize) {
var found = false
for _, price := range priceMaps {
if types.String(price["protectionBandwidth"]) == protectionSize {
found = true
}
}
if found {
allProtectionBandwidthSizes = append(allProtectionBandwidthSizes, protectionSize)
}
}
if !lists.ContainsString(allServerBandwidthSizes, serverSize) {
var found = false
for _, price := range priceMaps {
if types.String(price["serverBandwidth"]) == serverSize {
found = true
}
}
if found {
allServerBandwidthSizes = append(allServerBandwidthSizes, serverSize)
}
}
}
this.Data["allProtectionBandwidthSizes"] = allProtectionBandwidthSizes
this.Data["allServerBandwidthSizes"] = allServerBandwidthSizes
// 线路
var networkMaps = []maps.Map{}
networkResp, err := this.RPC().ADNetworkRPC().FindAllAvailableADNetworks(this.UserContext(), &pb.FindAllAvailableADNetworksRequest{})
if err != nil {
this.ErrorPage(err)
return
}
for _, network := range networkResp.AdNetworks {
var found = false
for _, price := range priceMaps {
if types.Int64(price["networkId"]) == network.Id {
found = true
}
}
if found {
networkMaps = append(networkMaps, maps.Map{
"id": network.Id,
"name": network.Name,
"description": network.Description,
})
}
}
this.Data["allNetworks"] = networkMaps
// 周期
var periodMaps = []maps.Map{}
periodResp, err := this.RPC().ADPackagePeriodRPC().FindAllAvailableADPackagePeriods(this.UserContext(), &pb.FindAllAvailableADPackagePeriodsRequest{})
if err != nil {
this.ErrorPage(err)
return
}
for _, period := range periodResp.AdPackagePeriods {
var found = false
for _, price := range priceMaps {
if types.Int64(price["periodId"]) == period.Id {
found = true
}
}
if found {
periodMaps = append(periodMaps, maps.Map{
"id": period.Id,
"count": period.Count,
"unit": period.Unit,
"unitName": userconfigs.ADPackagePeriodUnitName(period.Unit),
})
}
}
this.Data["allPeriods"] = periodMaps
this.Show()
}

View File

@@ -0,0 +1,54 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package packages
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/types"
)
type PriceAction struct {
actionutils.ParentAction
}
func (this *PriceAction) RunPost(params struct {
PackageId int64
PeriodId int64
Count int32
}) {
if params.PackageId <= 0 || params.PeriodId <= 0 || params.Count <= 0 {
this.Data["price"] = 0
this.Data["amount"] = 0
this.Success()
return
}
// 数量
countInstancesResp, err := this.RPC().ADPackageInstanceRPC().CountIdleADPackageInstances(this.UserContext(), &pb.CountIdleADPackageInstancesRequest{AdPackageId: params.PackageId})
if err != nil {
this.ErrorPage(err)
return
}
var countInstances = types.Int32(countInstancesResp.Count)
if countInstances < params.Count {
this.Data["price"] = 0
this.Data["amount"] = 0
this.Success()
return
}
resp, err := this.RPC().ADPackagePriceRPC().FindADPackagePrice(this.UserContext(), &pb.FindADPackagePriceRequest{
AdPackageId: params.PackageId,
AdPackagePeriodId: params.PeriodId,
Count: params.Count,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["price"] = resp.Price
this.Data["amount"] = resp.Amount
this.Success()
}

View File

@@ -0,0 +1 @@
支付、通知等URL

View File

@@ -0,0 +1,30 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package api
import (
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/api/pay/alipay"
serversapi "github.com/TeaOSLab/EdgeUser/internal/web/actions/default/api/servers"
domainsapi "github.com/TeaOSLab/EdgeUser/internal/web/actions/default/api/servers/domains"
"github.com/TeaOSLab/EdgeUser/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Prefix("/api").
// pay
Post("/pay/alipay/notify", new(alipay.NotifyAction)).
// servers
Helper(helpers.NewUserMustAuth("")).
Post("/user/servers/list", new(serversapi.ServersListAction)).
Post("/user/servers/domains/add", new(domainsapi.DomainsAddAction)).
Post("/user/servers/domains/delete", new(domainsapi.DomainsDeleteAction)).
//
EndAll()
})
}

View File

@@ -0,0 +1,41 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package alipay
import (
"encoding/json"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
"github.com/TeaOSLab/EdgeUser/internal/remotelogs"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/login/loginutils"
)
// NotifyAction TODO 防止cc攻击
type NotifyAction struct {
actionutils.ParentAction
}
func (this *NotifyAction) RunPost(params struct{}) {
formData, err := json.Marshal(this.Request.Form)
if err != nil {
// 提示中加入IP方便调试
remotelogs.Error("ALIPAY", "["+loginutils.RemoteIP(&this.ActionObject)+"]NotifyAction: encode form failed: "+err.Error())
this.Fail(err.Error())
return
}
// 接口会自动根据传输的订单号决定使用哪种支付方式验证
_, err = this.RPC().UserOrderRPC().NotifyUserOrderPayment(this.UserContext(), &pb.NotifyUserOrderPaymentRequest{
PayMethod: userconfigs.PayMethodAlipay,
FormData: formData,
})
if err != nil {
// 提示中加入IP方便调试
remotelogs.Error("ALIPAY", "["+loginutils.RemoteIP(&this.ActionObject)+"]NotifyAction: verify failed: "+err.Error())
this.Fail(err.Error())
return
}
this.Success()
}

View File

@@ -0,0 +1,280 @@
package api
import (
"bytes"
"encoding/json"
"errors"
"io"
"net/http"
"net/url"
"time"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/configs"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
)
type DomainsAddAction struct {
actionutils.ParentAction
}
func (this *DomainsAddAction) RunPost(params struct{}) {
// 获取当前登录用户ID
userId := this.UserId()
if userId <= 0 {
this.writeJSON(http.StatusUnauthorized, "请先登录", nil)
return
}
// 手动读取JSON请求体
body, err := io.ReadAll(this.Request.Body)
if err != nil {
this.writeJSON(http.StatusBadRequest, "读取请求体失败: "+err.Error(), nil)
return
}
// 解析JSON参数
var requestParams struct {
ServerId int64 `json:"serverId"`
Domains []string `json:"domains"`
}
err = json.Unmarshal(body, &requestParams)
if err != nil {
this.writeJSON(http.StatusBadRequest, "解析请求参数失败: "+err.Error(), nil)
return
}
// 参数验证
if requestParams.ServerId <= 0 {
this.writeJSON(http.StatusBadRequest, "serverId不能为空", nil)
return
}
if len(requestParams.Domains) == 0 {
this.writeJSON(http.StatusBadRequest, "domains不能为空", nil)
return
}
// 注意:权限验证由 UpdateServerWebConfigForUser 内部完成,这里不需要单独调用 CheckUserServer
// 获取Access Token
accessToken, err := this.getAccessToken(userId)
if err != nil {
this.writeJSON(http.StatusInternalServerError, "获取访问令牌失败: "+err.Error(), nil)
return
}
// 通过HTTP调用EdgeAPI的REST接口
apiConfig, err := configs.LoadAPIConfig()
if err != nil {
this.writeJSON(http.StatusInternalServerError, "无法获取API配置: "+err.Error(), nil)
return
}
if len(apiConfig.RPCEndpoints) == 0 {
this.writeJSON(http.StatusInternalServerError, "未配置RPC端点", nil)
return
}
// 使用第一个RPC端点转换为REST端点
rpcEndpoint := apiConfig.RPCEndpoints[0]
u, err := url.Parse(rpcEndpoint)
if err != nil {
this.writeJSON(http.StatusInternalServerError, "无效的RPC端点: "+err.Error(), nil)
return
}
// REST接口路径/ServerService/UpdateServerWebConfigForUser
restURL := u.Scheme + "://" + u.Host + "/ServerService/UpdateServerWebConfigForUser"
// 构建请求体
requestBody := map[string]interface{}{
"serverId": requestParams.ServerId,
"domains": requestParams.Domains,
"autoCreateCert": true,
}
requestJSON, err := json.Marshal(requestBody)
if err != nil {
this.writeJSON(http.StatusInternalServerError, "构建请求失败: "+err.Error(), nil)
return
}
// 创建HTTP请求
req, err := http.NewRequest("POST", restURL, bytes.NewReader(requestJSON))
if err != nil {
this.writeJSON(http.StatusInternalServerError, "创建请求失败: "+err.Error(), nil)
return
}
// 设置请求头
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Edge-Access-Token", accessToken)
// 发送请求
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
this.writeJSON(http.StatusInternalServerError, "调用API失败: "+err.Error(), nil)
return
}
defer resp.Body.Close()
// 读取响应
respBody, err := io.ReadAll(resp.Body)
if err != nil {
this.writeJSON(http.StatusInternalServerError, "读取响应失败: "+err.Error(), nil)
return
}
// 解析响应
var apiResp struct {
Code int `json:"code"`
Message string `json:"message"`
Data map[string]interface{} `json:"data"`
}
err = json.Unmarshal(respBody, &apiResp)
if err != nil {
this.writeJSON(http.StatusInternalServerError, "解析响应失败: "+err.Error(), nil)
return
}
if apiResp.Code != 200 {
this.writeJSON(apiResp.Code, apiResp.Message, nil)
return
}
// 格式化返回数据
result := make(map[string]string)
if resultData, ok := apiResp.Data["result"].(map[string]interface{}); ok {
if domains, ok := resultData["domains"].(map[string]interface{}); ok {
for domain, statusObj := range domains {
if statusMap, ok := statusObj.(map[string]interface{}); ok {
if status, ok := statusMap["status"].(string); ok {
if status == "success" {
result[domain] = "success"
} else {
reason := ""
if r, ok := statusMap["reason"].(string); ok {
reason = r
}
result[domain] = "fail: " + reason
}
}
}
}
}
}
this.writeJSON(http.StatusOK, "ok", map[string]interface{}{
"result": result,
})
}
// writeJSON 写入JSON响应
func (this *DomainsAddAction) writeJSON(code int, message string, data interface{}) {
this.ResponseWriter.Header().Set("Content-Type", "application/json; charset=utf-8")
this.ResponseWriter.WriteHeader(code)
response := map[string]interface{}{
"code": code,
"message": message,
"data": data,
}
jsonData, err := json.Marshal(response)
if err != nil {
this.ResponseWriter.WriteHeader(http.StatusInternalServerError)
this.ResponseWriter.Write([]byte(`{"code":500,"message":"marshal json failed","data":null}`))
return
}
this.ResponseWriter.Write(jsonData)
}
// getAccessToken 获取Access Token
func (this *DomainsAddAction) getAccessToken(userId int64) (string, error) {
// 获取用户的AccessKey
accessKeysResp, err := this.RPC().UserAccessKeyRPC().FindAllEnabledUserAccessKeys(this.UserContext(), &pb.FindAllEnabledUserAccessKeysRequest{
UserId: userId,
})
if err != nil {
return "", err
}
if len(accessKeysResp.UserAccessKeys) == 0 {
return "", errors.New("用户未配置AccessKey请先在设置中创建AccessKey")
}
// 使用第一个AccessKey
accessKey := accessKeysResp.UserAccessKeys[0]
// 获取EdgeAPI地址
apiConfig, err := configs.LoadAPIConfig()
if err != nil {
return "", err
}
if len(apiConfig.RPCEndpoints) == 0 {
return "", errors.New("未配置RPC端点")
}
// 调用GetAPIAccessToken接口
rpcEndpoint := apiConfig.RPCEndpoints[0]
u, err := url.Parse(rpcEndpoint)
if err != nil {
return "", err
}
restURL := u.Scheme + "://" + u.Host + "/APIAccessTokenService/getAPIAccessToken"
// 构建请求体
requestBody := map[string]interface{}{
"type": "user",
"accessKeyId": accessKey.UniqueId,
"accessKey": accessKey.Secret,
}
requestJSON, err := json.Marshal(requestBody)
if err != nil {
return "", err
}
// 创建HTTP请求
req, err := http.NewRequest("POST", restURL, bytes.NewReader(requestJSON))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
// 发送请求
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
// 读取响应
tokenRespBody, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
// 解析响应
var tokenResp struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
Token string `json:"token"`
ExpiresAt int64 `json:"expiresAt"`
} `json:"data"`
}
err = json.Unmarshal(tokenRespBody, &tokenResp)
if err != nil {
return "", err
}
if tokenResp.Code != 200 {
return "", errors.New(tokenResp.Message)
}
return tokenResp.Data.Token, nil
}

View File

@@ -0,0 +1,278 @@
package api
import (
"bytes"
"encoding/json"
"errors"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/configs"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"io"
"net/http"
"net/url"
"time"
)
type DomainsDeleteAction struct {
actionutils.ParentAction
}
func (this *DomainsDeleteAction) RunPost(params struct{}) {
// 获取当前登录用户ID
userId := this.UserId()
if userId <= 0 {
this.writeJSON(http.StatusUnauthorized, "请先登录", nil)
return
}
// 手动读取JSON请求体
body, err := io.ReadAll(this.Request.Body)
if err != nil {
this.writeJSON(http.StatusBadRequest, "读取请求体失败: "+err.Error(), nil)
return
}
// 解析JSON参数
var requestParams struct {
ServerId int64 `json:"serverId"`
Domains []string `json:"domains"`
}
err = json.Unmarshal(body, &requestParams)
if err != nil {
this.writeJSON(http.StatusBadRequest, "解析请求参数失败: "+err.Error(), nil)
return
}
// 参数验证
if requestParams.ServerId <= 0 {
this.writeJSON(http.StatusBadRequest, "serverId不能为空", nil)
return
}
if len(requestParams.Domains) == 0 {
this.writeJSON(http.StatusBadRequest, "domains不能为空", nil)
return
}
// 注意:权限验证由 ResetServerWebConfigForUser 内部完成,这里不需要单独调用 CheckUserServer
// 获取Access Token
accessToken, err := this.getAccessToken(userId)
if err != nil {
this.writeJSON(http.StatusInternalServerError, "获取访问令牌失败: "+err.Error(), nil)
return
}
// 通过HTTP调用EdgeAPI的REST接口
apiConfig, err := configs.LoadAPIConfig()
if err != nil {
this.writeJSON(http.StatusInternalServerError, "无法获取API配置: "+err.Error(), nil)
return
}
if len(apiConfig.RPCEndpoints) == 0 {
this.writeJSON(http.StatusInternalServerError, "未配置RPC端点", nil)
return
}
// 使用第一个RPC端点转换为REST端点
rpcEndpoint := apiConfig.RPCEndpoints[0]
u, err := url.Parse(rpcEndpoint)
if err != nil {
this.writeJSON(http.StatusInternalServerError, "无效的RPC端点: "+err.Error(), nil)
return
}
// REST接口路径/ServerService/ResetServerWebConfigForUser
restURL := u.Scheme + "://" + u.Host + "/ServerService/ResetServerWebConfigForUser"
// 构建请求体
requestBody := map[string]interface{}{
"serverId": requestParams.ServerId,
"domains": requestParams.Domains,
}
requestJSON, err := json.Marshal(requestBody)
if err != nil {
this.writeJSON(http.StatusInternalServerError, "构建请求失败: "+err.Error(), nil)
return
}
// 创建HTTP请求
req, err := http.NewRequest("POST", restURL, bytes.NewReader(requestJSON))
if err != nil {
this.writeJSON(http.StatusInternalServerError, "创建请求失败: "+err.Error(), nil)
return
}
// 设置请求头
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Edge-Access-Token", accessToken)
// 发送请求
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
this.writeJSON(http.StatusInternalServerError, "调用API失败: "+err.Error(), nil)
return
}
defer resp.Body.Close()
// 读取响应
respBody, err := io.ReadAll(resp.Body)
if err != nil {
this.writeJSON(http.StatusInternalServerError, "读取响应失败: "+err.Error(), nil)
return
}
// 解析响应
var apiResp struct {
Code int `json:"code"`
Message string `json:"message"`
Data map[string]interface{} `json:"data"`
}
err = json.Unmarshal(respBody, &apiResp)
if err != nil {
this.writeJSON(http.StatusInternalServerError, "解析响应失败: "+err.Error(), nil)
return
}
if apiResp.Code != 200 {
this.writeJSON(apiResp.Code, apiResp.Message, nil)
return
}
// 格式化返回数据
result := make(map[string]string)
if resultData, ok := apiResp.Data["result"].(map[string]interface{}); ok {
if domains, ok := resultData["domains"].(map[string]interface{}); ok {
for domain, statusObj := range domains {
if statusMap, ok := statusObj.(map[string]interface{}); ok {
if status, ok := statusMap["status"].(string); ok {
if status == "success" {
result[domain] = "success"
} else {
reason := ""
if r, ok := statusMap["reason"].(string); ok {
reason = r
}
result[domain] = "fail: " + reason
}
}
}
}
}
}
this.writeJSON(http.StatusOK, "ok", map[string]interface{}{
"result": result,
})
}
// writeJSON 写入JSON响应
func (this *DomainsDeleteAction) writeJSON(code int, message string, data interface{}) {
this.ResponseWriter.Header().Set("Content-Type", "application/json; charset=utf-8")
this.ResponseWriter.WriteHeader(code)
response := map[string]interface{}{
"code": code,
"message": message,
"data": data,
}
jsonData, err := json.Marshal(response)
if err != nil {
this.ResponseWriter.WriteHeader(http.StatusInternalServerError)
this.ResponseWriter.Write([]byte(`{"code":500,"message":"marshal json failed","data":null}`))
return
}
this.ResponseWriter.Write(jsonData)
}
// getAccessToken 获取Access Token
func (this *DomainsDeleteAction) getAccessToken(userId int64) (string, error) {
// 获取用户的AccessKey
accessKeysResp, err := this.RPC().UserAccessKeyRPC().FindAllEnabledUserAccessKeys(this.UserContext(), &pb.FindAllEnabledUserAccessKeysRequest{
UserId: userId,
})
if err != nil {
return "", err
}
if len(accessKeysResp.UserAccessKeys) == 0 {
return "", errors.New("用户未配置AccessKey请先在设置中创建AccessKey")
}
// 使用第一个AccessKey
accessKey := accessKeysResp.UserAccessKeys[0]
// 获取EdgeAPI地址
apiConfig, err := configs.LoadAPIConfig()
if err != nil {
return "", err
}
if len(apiConfig.RPCEndpoints) == 0 {
return "", errors.New("未配置RPC端点")
}
// 调用GetAPIAccessToken接口
rpcEndpoint := apiConfig.RPCEndpoints[0]
u, err := url.Parse(rpcEndpoint)
if err != nil {
return "", err
}
restURL := u.Scheme + "://" + u.Host + "/APIAccessTokenService/getAPIAccessToken"
// 构建请求体
requestBody := map[string]interface{}{
"type": "user",
"accessKeyId": accessKey.UniqueId,
"accessKey": accessKey.Secret,
}
requestJSON, err := json.Marshal(requestBody)
if err != nil {
return "", err
}
// 创建HTTP请求
req, err := http.NewRequest("POST", restURL, bytes.NewReader(requestJSON))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
// 发送请求
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
// 读取响应
tokenRespBody, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
// 解析响应
var tokenResp struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
Token string `json:"token"`
ExpiresAt int64 `json:"expiresAt"`
} `json:"data"`
}
err = json.Unmarshal(tokenRespBody, &tokenResp)
if err != nil {
return "", err
}
if tokenResp.Code != 200 {
return "", errors.New(tokenResp.Message)
}
return tokenResp.Data.Token, nil
}

View File

@@ -0,0 +1,74 @@
package api
import (
"encoding/json"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"net/http"
)
type ServersListAction struct {
actionutils.ParentAction
}
func (this *ServersListAction) RunPost(params struct {
Username string `json:"username"`
}) {
// 获取当前登录用户ID
userId := this.UserId()
if userId <= 0 {
this.writeJSON(http.StatusUnauthorized, "请先登录", nil)
return
}
// 如果提供了username验证是否为当前用户可选验证
if len(params.Username) > 0 {
// 可以通过RPC获取用户信息并验证这里简化处理
// 实际使用时如果提供了username应该验证是否为当前用户
}
// 调用RPC获取用户的所有网站
resp, err := this.RPC().ServerRPC().FindAllUserServers(this.UserContext(), &pb.FindAllUserServersRequest{
UserId: userId,
})
if err != nil {
this.ErrorPage(err)
return
}
// 格式化返回数据
var servers []map[string]interface{}
for _, server := range resp.Servers {
servers = append(servers, map[string]interface{}{
"id": server.Id,
"isOn": server.IsOn,
"name": server.Name,
"firstServerName": server.FirstServerName,
})
}
this.writeJSON(http.StatusOK, "ok", map[string]interface{}{
"servers": servers,
})
}
// writeJSON 写入JSON响应
func (this *ServersListAction) writeJSON(code int, message string, data interface{}) {
this.ResponseWriter.Header().Set("Content-Type", "application/json; charset=utf-8")
this.ResponseWriter.WriteHeader(code)
response := map[string]interface{}{
"code": code,
"message": message,
"data": data,
}
jsonData, err := json.Marshal(response)
if err != nil {
this.ResponseWriter.WriteHeader(http.StatusInternalServerError)
this.ResponseWriter.Write([]byte(`{"code":500,"message":"marshal json failed","data":null}`))
return
}
this.ResponseWriter.Write(jsonData)
}

View File

@@ -0,0 +1,12 @@
package csrf
import "github.com/iwind/TeaGo"
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Prefix("/csrf").
Get("/token", new(TokenAction)).
EndAll()
})
}

View File

@@ -0,0 +1,39 @@
package csrf
import (
"github.com/TeaOSLab/EdgeUser/internal/csrf"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeUser/internal/web/helpers"
"sync"
"time"
)
var lastTimestamp = int64(0)
var locker sync.Mutex
type TokenAction struct {
actionutils.ParentAction
}
func (this *TokenAction) Init() {
this.Nav("", "", "")
}
func (this *TokenAction) RunGet(params struct {
Auth *helpers.UserShouldAuth
}) {
locker.Lock()
defer locker.Unlock()
defer func() {
lastTimestamp = time.Now().UnixNano()
}()
// 没有登录,则限制请求速度
if params.Auth.UserId() <= 0 && lastTimestamp > 0 && time.Now().UnixNano()-lastTimestamp <= 300_000_000 {
this.Fail("请求速度过快,请稍后刷新后重试")
}
this.Data["token"] = csrf.Generate()
this.Success()
}

View File

@@ -0,0 +1,179 @@
package dashboard
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/systemconfigs"
"github.com/TeaOSLab/EdgeUser/internal/configloaders"
"github.com/TeaOSLab/EdgeUser/internal/utils/numberutils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "", "")
}
func (this *IndexAction) RunGet(params struct{}) {
// 是否开启了CDN
registerConfig, err := configloaders.LoadRegisterConfig()
if err != nil {
this.ErrorPage(err)
return
}
if registerConfig != nil && !registerConfig.CDNIsOn {
if registerConfig.NSIsOn {
this.RedirectURL("/dashboard/ns")
return
}
// 显示空白
this.View("@blank")
this.Show()
return
}
// 是否需要审核和实名认证
this.Data["isVerified"] = this.Context.GetBool("isVerified")
this.Data["isIdentified"] = this.Context.GetBool("isIdentified")
if !configloaders.RequireVerification() {
this.Data["isVerified"] = true
}
if !configloaders.RequireIdentity() {
this.Data["isIdentified"] = true
}
// 查看UI配置
uiConfig, _ := configloaders.LoadUIConfig()
this.Data["uiConfig"] = maps.Map{
"showTrafficCharts": true,
"showBandwidthCharts": true,
"bandwidthUnit": systemconfigs.BandwidthUnitBit,
}
if uiConfig != nil {
this.Data["uiConfig"] = maps.Map{
"showTrafficCharts": uiConfig.ShowTrafficCharts,
"showBandwidthCharts": uiConfig.ShowBandwidthCharts,
"bandwidthUnit": systemconfigs.BandwidthUnitBit, // 强制使用比特单位
}
}
// 是否需要激活邮箱
var userResp *pb.FindEnabledUserResponse
this.Data["emailVerificationMessage"] = ""
if registerConfig != nil && registerConfig.EmailVerification.IsOn {
// 尚未激活
verificationResp, err := this.RPC().UserEmailVerificationRPC().FindLatestUserEmailVerification(this.UserContext(), &pb.FindLatestUserEmailVerificationRequest{})
if err != nil {
this.ErrorPage(err)
return
}
if verificationResp.UserEmailVerification != nil {
this.Data["emailVerificationMessage"] = "电子邮箱等待激活,请及时激活。"
} else if registerConfig.EmailVerification.ShowNotice {
// 尚未绑定
var userErr error
userResp, userErr = this.RPC().UserRPC().FindEnabledUser(this.UserContext(), &pb.FindEnabledUserRequest{UserId: this.UserId()})
if userErr != nil {
this.ErrorPage(userErr)
return
}
if userResp.User != nil && len(userResp.User.VerifiedEmail) == 0 {
this.Data["emailVerificationMessage"] = "尚未绑定电子邮箱,请点此绑定。"
}
}
}
// 是否需要验证手机号
this.Data["mobileVerificationMessage"] = ""
if registerConfig != nil && registerConfig.MobileVerification.IsOn && registerConfig.MobileVerification.ShowNotice {
// 尚未绑定
if userResp == nil {
var userErr error
userResp, userErr = this.RPC().UserRPC().FindEnabledUser(this.UserContext(), &pb.FindEnabledUserRequest{UserId: this.UserId()})
if userErr != nil {
this.ErrorPage(userErr)
return
}
}
if userResp.User != nil && len(userResp.User.VerifiedMobile) == 0 {
this.Data["mobileVerificationMessage"] = "尚未绑定手机号码,请点此绑定。"
}
}
this.Show()
}
func (this *IndexAction) RunPost(params struct{}) {
// 查看UI配置
uiConfig, _ := configloaders.LoadUIConfig()
this.Data["uiConfig"] = maps.Map{
"showTrafficCharts": true,
"showBandwidthCharts": true,
"bandwidthUnit": systemconfigs.BandwidthUnitBit,
}
if uiConfig != nil {
this.Data["uiConfig"] = maps.Map{
"showTrafficCharts": uiConfig.ShowTrafficCharts,
"showBandwidthCharts": uiConfig.ShowBandwidthCharts,
"bandwidthUnit": systemconfigs.BandwidthUnitBit, // 强制使用比特单位
}
}
dashboardResp, err := this.RPC().UserRPC().ComposeUserDashboard(this.UserContext(), &pb.ComposeUserDashboardRequest{UserId: this.UserId()})
if err != nil {
this.ErrorPage(err)
return
}
var monthlyPeekBandwidthBytes = dashboardResp.MonthlyPeekBandwidthBytes
var dailyPeekBandwidthBytes = dashboardResp.DailyPeekBandwidthBytes
monthlyPeekBandwidthBytes *= 8
dailyPeekBandwidthBytes *= 8
this.Data["dashboard"] = maps.Map{
"countServers": dashboardResp.CountServers,
"monthlyTrafficBytes": numberutils.FormatBytes(dashboardResp.MonthlyTrafficBytes),
"monthlyPeekBandwidthBytes": numberutils.FormatBits(monthlyPeekBandwidthBytes),
"dailyTrafficBytes": numberutils.FormatBytes(dashboardResp.DailyTrafficBytes),
"dailyPeekBandwidthBytes": numberutils.FormatBits(dailyPeekBandwidthBytes),
}
// 每日流量统计
{
var statMaps = []maps.Map{}
for _, stat := range dashboardResp.DailyTrafficStats {
var infoMap = maps.Map{
"bytes": stat.Bytes,
"day": stat.Day[4:6] + "月" + stat.Day[6:] + "日",
"countRequests": stat.CountRequests,
}
if uiConfig.ShowCacheInfoInTrafficCharts {
infoMap["cachedBytes"] = stat.CachedBytes
infoMap["countCachedRequests"] = stat.CountCachedRequests
}
statMaps = append(statMaps, infoMap)
}
this.Data["dailyTrafficStats"] = statMaps
}
// 每日峰值带宽统计
{
var statMaps = []maps.Map{}
for _, stat := range dashboardResp.DailyPeekBandwidthStats {
statMaps = append(statMaps, maps.Map{
"bytes": stat.Bytes,
"day": stat.Day[4:6] + "月" + stat.Day[6:] + "日",
})
}
this.Data["dailyPeekBandwidthStats"] = statMaps
}
this.Data["bandwidthPercentile"] = dashboardResp.BandwidthPercentile
this.Data["bandwidthPercentileBits"] = dashboardResp.BandwidthPercentileBits
this.Success()
}

View File

@@ -0,0 +1,18 @@
package dashboard
import (
"github.com/TeaOSLab/EdgeUser/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth("")).
Data("teaMenu", "dashboard").
Prefix("/dashboard").
GetPost("", new(IndexAction)).
Get("/ns", new(NsAction)).
EndAll()
})
}

View File

@@ -0,0 +1,67 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package dashboard
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/configloaders"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
)
type NsAction struct {
actionutils.ParentAction
}
func (this *NsAction) Init() {
this.Nav("", "", "")
}
func (this *NsAction) RunGet(params struct{}) {
// 是否开启了CDN
registerConfig, err := configloaders.LoadRegisterConfig()
if err != nil {
this.ErrorPage(err)
return
}
if registerConfig != nil && !registerConfig.NSIsOn {
// 显示空白
this.View("@blank")
this.Show()
return
}
resp, err := this.RPC().NSRPC().ComposeNSUserBoard(this.UserContext(), &pb.ComposeNSUserBoardRequest{})
if err != nil {
this.ErrorPage(err)
return
}
var planName = ""
if resp.NsUserPlan != nil && resp.NsUserPlan.NsPlan != nil {
planName = resp.NsUserPlan.NsPlan.Name
}
this.Data["board"] = maps.Map{
"countDomains": resp.CountNSDomains,
"countRecords": resp.CountNSRecords,
"countRoutes": resp.CountNSRoutes,
"planName": planName,
}
// 域名排行
{
var statMaps = []maps.Map{}
for _, stat := range resp.TopNSDomainStats {
statMaps = append(statMaps, maps.Map{
"domainId": stat.NsDomainId,
"domainName": stat.NsDomainName,
"countRequests": stat.CountRequests,
"bytes": stat.Bytes,
})
}
this.Data["topDomainStats"] = statMaps
}
this.Show()
}

View File

@@ -0,0 +1,39 @@
package dns
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
)
// DomainOptionsAction 域名列表选项
type DomainOptionsAction struct {
actionutils.ParentAction
}
func (this *DomainOptionsAction) RunPost(params struct {
ProviderId int64
}) {
domainsResp, err := this.RPC().DNSDomainRPC().FindAllBasicDNSDomainsWithDNSProviderId(this.UserContext(), &pb.FindAllBasicDNSDomainsWithDNSProviderIdRequest{
DnsProviderId: params.ProviderId,
})
if err != nil {
this.ErrorPage(err)
return
}
domainMaps := []maps.Map{}
for _, domain := range domainsResp.DnsDomains {
// 未开启或者已删除的先跳过
if !domain.IsOn || domain.IsDeleted {
continue
}
domainMaps = append(domainMaps, maps.Map{
"id": domain.Id,
"name": domain.Name,
})
}
this.Data["domains"] = domainMaps
this.Success()
}

View File

@@ -0,0 +1,73 @@
package domains
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
)
type ClustersPopupAction struct {
actionutils.ParentAction
}
func (this *ClustersPopupAction) Init() {
this.Nav("", "", "")
}
func (this *ClustersPopupAction) RunGet(params struct {
DomainId int64
}) {
// 域名信息
domainResp, err := this.RPC().DNSDomainRPC().FindBasicDNSDomain(this.UserContext(), &pb.FindBasicDNSDomainRequest{
DnsDomainId: params.DomainId,
})
if err != nil {
this.ErrorPage(err)
return
}
domain := domainResp.DnsDomain
if domain == nil {
this.NotFound("dnsDomain", params.DomainId)
return
}
this.Data["domain"] = domain.Name
// 集群
clustersResp, err := this.RPC().NodeClusterRPC().FindAllEnabledNodeClustersWithDNSDomainId(this.UserContext(), &pb.FindAllEnabledNodeClustersWithDNSDomainIdRequest{DnsDomainId: params.DomainId})
if err != nil {
this.ErrorPage(err)
return
}
clusterMaps := []maps.Map{}
for _, cluster := range clustersResp.NodeClusters {
isOk := false
if len(cluster.Name) > 0 {
for _, recordType := range []string{"A", "AAAA"} {
checkResp, err := this.RPC().DNSDomainRPC().ExistDNSDomainRecord(this.UserContext(), &pb.ExistDNSDomainRecordRequest{
DnsDomainId: params.DomainId,
Name: cluster.DnsName,
Type: recordType,
})
if err != nil {
this.ErrorPage(err)
return
}
if checkResp.IsOk {
isOk = true
break
}
}
}
clusterMaps = append(clusterMaps, maps.Map{
"id": cluster.Id,
"name": cluster.Name,
"dnsName": cluster.DnsName,
"isOk": isOk,
})
}
this.Data["clusters"] = clusterMaps
this.Show()
}

View File

@@ -0,0 +1,60 @@
package domains
import (
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/dns/domains/domainutils"
"github.com/iwind/TeaGo/actions"
"strings"
)
type CreatePopupAction struct {
actionutils.ParentAction
}
func (this *CreatePopupAction) Init() {
this.Nav("", "", "")
}
func (this *CreatePopupAction) RunGet(params struct {
ProviderId int64
}) {
this.Data["providerId"] = params.ProviderId
this.Show()
}
func (this *CreatePopupAction) RunPost(params struct {
ProviderId int64
Name string
Must *actions.Must
CSRF *actionutils.CSRF
}) {
// TODO 检查ProviderId
params.Must.
Field("name", params.Name).
Require("请输入域名")
// 校验域名
domain := strings.ToLower(params.Name)
domain = strings.Replace(domain, " ", "", -1)
if !domainutils.ValidateDomainFormat(domain) {
this.Fail("域名格式不正确,请修改后重新提交")
}
createResp, err := this.RPC().DNSDomainRPC().CreateDNSDomain(this.UserContext(), &pb.CreateDNSDomainRequest{
DnsProviderId: params.ProviderId,
Name: domain,
})
if err != nil {
this.ErrorPage(err)
return
}
defer this.CreateLogInfo(codes.DNS_LogCreateDomain, createResp.DnsDomainId)
this.Success()
}

View File

@@ -0,0 +1,38 @@
package domains
import (
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/utils/numberutils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
)
type DeleteAction struct {
actionutils.ParentAction
}
func (this *DeleteAction) RunPost(params struct {
DomainId int64
}) {
// 记录日志
defer this.CreateLogInfo(codes.DNS_LogDeleteDomain, params.DomainId)
// 检查是否正在使用
countResp, err := this.RPC().NodeClusterRPC().CountAllEnabledNodeClustersWithDNSDomainId(this.UserContext(), &pb.CountAllEnabledNodeClustersWithDNSDomainIdRequest{DnsDomainId: params.DomainId})
if err != nil {
this.ErrorPage(err)
return
}
if countResp.Count > 0 {
this.Fail("当前域名正在被" + numberutils.FormatInt64(countResp.Count) + "个集群所使用,所以不能删除。请修改后再操作。")
}
// 执行删除
_, err = this.RPC().DNSDomainRPC().DeleteDNSDomain(this.UserContext(), &pb.DeleteDNSDomainRequest{DnsDomainId: params.DomainId})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,149 @@
package domainutils
import (
"github.com/TeaOSLab/EdgeCommon/pkg/dnsconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/lists"
"github.com/iwind/TeaGo/maps"
"net"
"regexp"
"strings"
)
// ValidateDomainFormat 校验域名格式
func ValidateDomainFormat(domain string) bool {
pieces := strings.Split(domain, ".")
for _, piece := range pieces {
if piece == "-" ||
strings.HasPrefix(piece, "-") ||
strings.HasSuffix(piece, "-") ||
len(piece) > 63 ||
// 我们允许中文、大写字母、下划线,防止有些特殊场景下需要
!regexp.MustCompile(`^[\p{Han}_a-zA-Z0-9-]+$`).MatchString(piece) {
return false
}
}
// 最后一段不能是全数字
if regexp.MustCompile(`^(\d+)$`).MatchString(pieces[len(pieces)-1]) {
return false
}
return true
}
// ConvertRoutesToMaps 转换线路列表
func ConvertRoutesToMaps(info *pb.NodeDNSInfo) []maps.Map {
if info == nil {
return []maps.Map{}
}
result := []maps.Map{}
for _, route := range info.Routes {
result = append(result, maps.Map{
"name": route.Name,
"code": route.Code,
"domainId": info.DnsDomainId,
"domainName": info.DnsDomainName,
})
}
return result
}
// FilterRoutes 筛选线路
func FilterRoutes(routes []*pb.DNSRoute, allRoutes []*pb.DNSRoute) []*pb.DNSRoute {
routeCodes := []string{}
for _, route := range allRoutes {
routeCodes = append(routeCodes, route.Code)
}
result := []*pb.DNSRoute{}
for _, route := range routes {
if lists.ContainsString(routeCodes, route.Code) {
result = append(result, route)
}
}
return result
}
// ValidateRecordName 校验记录名
func ValidateRecordName(name string) bool {
if name == "*" || name == "@" || len(name) == 0 {
return true
}
pieces := strings.Split(name, ".")
for index, piece := range pieces {
if index == 0 && piece == "*" {
continue
}
if piece == "-" ||
strings.HasPrefix(piece, "-") ||
strings.HasSuffix(piece, "-") ||
//strings.Contains(piece, "--") ||
len(piece) > 63 ||
// 我们允许中文、大写字母、下划线,防止有些特殊场景下需要
!regexp.MustCompile(`^[\p{Han}_a-zA-Z0-9-]+$`).MatchString(piece) {
return false
}
}
return true
}
// ValidateRecordValue 校验记录值
func ValidateRecordValue(recordType dnsconfigs.RecordType, value string) (message string, ok bool) {
switch recordType {
case dnsconfigs.RecordTypeA:
if !regexp.MustCompile(`^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$`).MatchString(value) {
message = "请输入正确格式的IP"
return
}
if net.ParseIP(value) == nil {
message = "请输入正确格式的IP"
return
}
case dnsconfigs.RecordTypeCNAME:
value = strings.TrimSuffix(value, ".")
if !strings.Contains(value, ".") || !ValidateDomainFormat(value) {
message = "请输入正确的域名"
return
}
case dnsconfigs.RecordTypeAAAA:
if !strings.Contains(value, ":") {
message = "请输入正确格式的IPv6地址"
return
}
if net.ParseIP(value) == nil {
message = "请输入正确格式的IPv6地址"
return
}
case dnsconfigs.RecordTypeNS:
value = strings.TrimSuffix(value, ".")
if !strings.Contains(value, ".") || !ValidateDomainFormat(value) {
message = "请输入正确的DNS服务器域名"
return
}
case dnsconfigs.RecordTypeMX:
value = strings.TrimSuffix(value, ".")
if !strings.Contains(value, ".") || !ValidateDomainFormat(value) {
message = "请输入正确的邮件服务器域名"
return
}
case dnsconfigs.RecordTypeSRV:
if len(value) == 0 {
message = "请输入主机名"
return
}
case dnsconfigs.RecordTypeTXT:
if len(value) > 512 {
message = "文本长度不能超出512字节"
return
}
}
if len(value) > 512 {
message = "记录值长度不能超出512字节"
return
}
ok = true
return
}

View File

@@ -0,0 +1,61 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package domainutils
import (
"github.com/TeaOSLab/EdgeCommon/pkg/dnsconfigs"
"github.com/iwind/TeaGo/assert"
"testing"
)
func TestValidateRecordValue(t *testing.T) {
a := assert.NewAssertion(t)
// A
{
_, ok := ValidateRecordValue(dnsconfigs.RecordTypeA, "1.2")
a.IsFalse(ok)
}
{
_, ok := ValidateRecordValue(dnsconfigs.RecordTypeA, "1.2.3.400")
a.IsFalse(ok)
}
{
_, ok := ValidateRecordValue(dnsconfigs.RecordTypeA, "1.2.3.4")
a.IsTrue(ok)
}
// CNAME
{
_, ok := ValidateRecordValue(dnsconfigs.RecordTypeCNAME, "example.com")
a.IsTrue(ok)
}
{
_, ok := ValidateRecordValue(dnsconfigs.RecordTypeCNAME, "example.com.")
a.IsTrue(ok)
}
{
_, ok := ValidateRecordValue(dnsconfigs.RecordTypeCNAME, "hello, world")
a.IsFalse(ok)
}
// AAAA
{
_, ok := ValidateRecordValue(dnsconfigs.RecordTypeAAAA, "1.2.3.4")
a.IsFalse(ok)
}
{
_, ok := ValidateRecordValue(dnsconfigs.RecordTypeAAAA, "2001:0db8:85a3:0000:0000:8a2e:0370:7334")
a.IsTrue(ok)
}
// NS
{
_, ok := ValidateRecordValue(dnsconfigs.RecordTypeNS, "1.2.3.4")
a.IsFalse(ok)
}
{
_, ok := ValidateRecordValue(dnsconfigs.RecordTypeNS, "example.com")
a.IsTrue(ok)
}
}

View File

@@ -0,0 +1,146 @@
package domains
import (
"github.com/TeaOSLab/EdgeCommon/pkg/iputils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
)
type NodesPopupAction struct {
actionutils.ParentAction
}
func (this *NodesPopupAction) Init() {
this.Nav("", "", "")
}
func (this *NodesPopupAction) RunGet(params struct {
DomainId int64
}) {
this.Data["domainId"] = params.DomainId
// 域名信息
domainResp, err := this.RPC().DNSDomainRPC().FindBasicDNSDomain(this.UserContext(), &pb.FindBasicDNSDomainRequest{
DnsDomainId: params.DomainId,
})
if err != nil {
this.ErrorPage(err)
return
}
var domain = domainResp.DnsDomain
if domain == nil {
this.NotFound("dnsDomain", params.DomainId)
return
}
this.Data["domain"] = domain.Name
// 集群
var clusterMaps = []maps.Map{}
clustersResp, err := this.RPC().NodeClusterRPC().FindAllEnabledNodeClustersWithDNSDomainId(this.UserContext(), &pb.FindAllEnabledNodeClustersWithDNSDomainIdRequest{DnsDomainId: params.DomainId})
if err != nil {
this.ErrorPage(err)
return
}
for _, cluster := range clustersResp.NodeClusters {
// 默认值
var defaultRoute = cluster.DnsDefaultRoute
// 节点DNS解析记录
nodesResp, err := this.RPC().NodeRPC().FindAllEnabledNodesDNSWithNodeClusterId(this.UserContext(), &pb.FindAllEnabledNodesDNSWithNodeClusterIdRequest{
NodeClusterId: cluster.Id,
IsInstalled: true,
})
if err != nil {
this.ErrorPage(err)
return
}
var nodeMaps = []maps.Map{}
for _, node := range nodesResp.Nodes {
if len(node.Routes) > 0 {
for _, route := range node.Routes {
// 检查是否有域名解析记录
var isResolved = false
if len(route.Name) > 0 && len(node.IpAddr) > 0 && len(cluster.DnsName) > 0 {
var recordType = "A"
if iputils.IsIPv6(node.IpAddr) {
recordType = "AAAA"
}
checkResp, err := this.RPC().DNSDomainRPC().ExistDNSDomainRecord(this.UserContext(), &pb.ExistDNSDomainRecordRequest{
DnsDomainId: params.DomainId,
Name: cluster.DnsName,
Type: recordType,
Route: route.Code,
Value: node.IpAddr,
})
if err != nil {
this.ErrorPage(err)
return
}
isResolved = checkResp.IsOk
}
nodeMaps = append(nodeMaps, maps.Map{
"id": node.Id,
"name": node.Name,
"ipAddr": node.IpAddr,
"route": maps.Map{
"name": route.Name,
"code": route.Code,
},
"clusterId": node.NodeClusterId,
"isOk": isResolved,
})
}
} else {
// 默认线路
var isResolved = false
if len(defaultRoute) > 0 {
var recordType = "A"
if iputils.IsIPv6(node.IpAddr) {
recordType = "AAAA"
}
checkResp, err := this.RPC().DNSDomainRPC().ExistDNSDomainRecord(this.UserContext(), &pb.ExistDNSDomainRecordRequest{
DnsDomainId: cluster.DnsDomainId,
Name: cluster.DnsName,
Type: recordType,
Route: defaultRoute,
Value: node.IpAddr,
})
if err != nil {
this.ErrorPage(err)
return
}
isResolved = checkResp.IsOk
}
nodeMaps = append(nodeMaps, maps.Map{
"id": node.Id,
"name": node.Name,
"ipAddr": node.IpAddr,
"route": maps.Map{
"name": "",
"code": "",
},
"clusterId": node.NodeClusterId,
"isOk": isResolved,
})
}
}
if len(nodeMaps) == 0 {
continue
}
clusterMaps = append(clusterMaps, maps.Map{
"id": cluster.Id,
"name": cluster.Name,
"dnsName": cluster.DnsName,
"nodes": nodeMaps,
})
}
this.Data["clusters"] = clusterMaps
this.Show()
}

View File

@@ -0,0 +1,27 @@
package domains
import (
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
)
type RecoverAction struct {
actionutils.ParentAction
}
func (this *RecoverAction) RunPost(params struct {
DomainId int64
}) {
// 记录日志
defer this.CreateLogInfo(codes.DNS_LogRecoverDomain, params.DomainId)
// 执行恢复
_, err := this.RPC().DNSDomainRPC().RecoverDNSDomain(this.UserContext(), &pb.RecoverDNSDomainRequest{DnsDomainId: params.DomainId})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,35 @@
package domains
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
)
type RoutesPopupAction struct {
actionutils.ParentAction
}
func (this *RoutesPopupAction) Init() {
this.Nav("", "", "")
}
func (this *RoutesPopupAction) RunGet(params struct {
DomainId int64
}) {
routesResp, err := this.RPC().DNSDomainRPC().FindAllDNSDomainRoutes(this.UserContext(), &pb.FindAllDNSDomainRoutesRequest{DnsDomainId: params.DomainId})
if err != nil {
this.ErrorPage(err)
return
}
routeMaps := []maps.Map{}
for _, route := range routesResp.Routes {
routeMaps = append(routeMaps, maps.Map{
"name": route.Name,
"code": route.Code,
})
}
this.Data["routes"] = routeMaps
this.Show()
}

View File

@@ -0,0 +1,105 @@
package domains
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
)
type SelectPopupAction struct {
actionutils.ParentAction
}
func (this *SelectPopupAction) Init() {
this.Nav("", "", "")
}
func (this *SelectPopupAction) RunGet(params struct {
DomainId int64
}) {
this.Data["domainId"] = 0
this.Data["domainName"] = ""
this.Data["providerId"] = 0
this.Data["providerType"] = ""
// 域名信息
if params.DomainId > 0 {
domainResp, err := this.RPC().DNSDomainRPC().FindDNSDomain(this.UserContext(), &pb.FindDNSDomainRequest{DnsDomainId: params.DomainId})
if err != nil {
this.ErrorPage(err)
return
}
var domain = domainResp.DnsDomain
if domain != nil {
this.Data["domainId"] = domain.Id
this.Data["domainName"] = domain.Name
this.Data["providerId"] = domain.ProviderId
providerResp, err := this.RPC().DNSProviderRPC().FindEnabledDNSProvider(this.UserContext(), &pb.FindEnabledDNSProviderRequest{DnsProviderId: domain.ProviderId})
if err != nil {
this.ErrorPage(err)
return
}
if providerResp.DnsProvider != nil {
this.Data["providerType"] = providerResp.DnsProvider.Type
}
}
}
// 所有服务商
providerTypesResp, err := this.RPC().DNSProviderRPC().FindAllDNSProviderTypes(this.UserContext(), &pb.FindAllDNSProviderTypesRequest{})
if err != nil {
this.ErrorPage(err)
return
}
var providerTypeMaps = []maps.Map{}
for _, providerType := range providerTypesResp.ProviderTypes {
providerTypeMaps = append(providerTypeMaps, maps.Map{
"name": providerType.Name,
"code": providerType.Code,
})
}
this.Data["providerTypes"] = providerTypeMaps
this.Show()
}
func (this *SelectPopupAction) RunPost(params struct {
DomainId int64
Must *actions.Must
CSRF *actionutils.CSRF
}) {
this.Data["domainId"] = params.DomainId
this.Data["domainName"] = ""
this.Data["providerName"] = ""
if params.DomainId > 0 {
domainResp, err := this.RPC().DNSDomainRPC().FindDNSDomain(this.UserContext(), &pb.FindDNSDomainRequest{DnsDomainId: params.DomainId})
if err != nil {
this.ErrorPage(err)
return
}
if domainResp.DnsDomain != nil {
this.Data["domainName"] = domainResp.DnsDomain.Name
// 服务商名称
var providerId = domainResp.DnsDomain.ProviderId
if providerId > 0 {
providerResp, err := this.RPC().DNSProviderRPC().FindEnabledDNSProvider(this.UserContext(), &pb.FindEnabledDNSProviderRequest{DnsProviderId: providerId})
if err != nil {
this.ErrorPage(err)
return
}
if providerResp.DnsProvider != nil {
this.Data["providerName"] = providerResp.DnsProvider.Name
}
}
} else {
this.Data["domainId"] = 0
}
}
this.Success()
}

View File

@@ -0,0 +1,85 @@
package domains
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
)
type ServersPopupAction struct {
actionutils.ParentAction
}
func (this *ServersPopupAction) Init() {
this.Nav("", "", "")
}
func (this *ServersPopupAction) RunGet(params struct {
DomainId int64
}) {
this.Data["domainId"] = params.DomainId
// 域名信息
domainResp, err := this.RPC().DNSDomainRPC().FindBasicDNSDomain(this.UserContext(), &pb.FindBasicDNSDomainRequest{
DnsDomainId: params.DomainId,
})
if err != nil {
this.ErrorPage(err)
return
}
var domain = domainResp.DnsDomain
if domain == nil {
this.NotFound("dnsDomain", params.DomainId)
return
}
this.Data["domain"] = domain.Name
// 服务信息
var clusterMaps = []maps.Map{}
clustersResp, err := this.RPC().NodeClusterRPC().FindAllEnabledNodeClustersWithDNSDomainId(this.UserContext(), &pb.FindAllEnabledNodeClustersWithDNSDomainIdRequest{DnsDomainId: params.DomainId})
if err != nil {
this.ErrorPage(err)
return
}
for _, cluster := range clustersResp.NodeClusters {
serversResp, err := this.RPC().ServerRPC().FindAllEnabledServersDNSWithNodeClusterId(this.UserContext(), &pb.FindAllEnabledServersDNSWithNodeClusterIdRequest{NodeClusterId: cluster.Id})
if err != nil {
this.ErrorPage(err)
return
}
var serverMaps = []maps.Map{}
for _, server := range serversResp.Servers {
var isOk = false
if len(cluster.DnsName) > 0 && len(server.DnsName) > 0 {
checkResp, err := this.RPC().DNSDomainRPC().ExistDNSDomainRecord(this.UserContext(), &pb.ExistDNSDomainRecordRequest{
DnsDomainId: params.DomainId,
Name: server.DnsName,
Type: "CNAME",
Value: cluster.DnsName + "." + domain.Name,
})
if err != nil {
this.ErrorPage(err)
return
}
isOk = checkResp.IsOk
}
serverMaps = append(serverMaps, maps.Map{
"id": server.Id,
"name": server.Name,
"dnsName": server.DnsName,
"isOk": isOk,
})
}
clusterMaps = append(clusterMaps, maps.Map{
"id": cluster.Id,
"name": cluster.Name,
"dnsName": cluster.DnsName,
"servers": serverMaps,
})
}
this.Data["clusters"] = clusterMaps
this.Show()
}

View File

@@ -0,0 +1,33 @@
package domains
import (
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
)
type SyncAction struct {
actionutils.ParentAction
}
func (this *SyncAction) RunPost(params struct {
DomainId int64
}) {
// 记录日志
defer this.CreateLogInfo(codes.DNS_LogSyncDomain, params.DomainId)
// 执行同步
resp, err := this.RPC().DNSDomainRPC().SyncDNSDomainData(this.UserContext(), &pb.SyncDNSDomainDataRequest{DnsDomainId: params.DomainId})
if err != nil {
this.ErrorPage(err)
return
}
if resp.IsOk {
this.Success()
} else {
this.Data["shouldFix"] = resp.ShouldFix
this.Fail(resp.Error)
}
this.Success()
}

View File

@@ -0,0 +1,79 @@
package domains
import (
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/dns/domains/domainutils"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
"strings"
)
type UpdatePopupAction struct {
actionutils.ParentAction
}
func (this *UpdatePopupAction) Init() {
this.Nav("", "", "")
}
func (this *UpdatePopupAction) RunGet(params struct {
DomainId int64
}) {
domainResp, err := this.RPC().DNSDomainRPC().FindDNSDomain(this.UserContext(), &pb.FindDNSDomainRequest{DnsDomainId: params.DomainId})
if err != nil {
this.ErrorPage(err)
return
}
domain := domainResp.DnsDomain
if domain == nil {
this.NotFound("dnsDomain", params.DomainId)
return
}
this.Data["domain"] = maps.Map{
"id": domain.Id,
"name": domain.Name,
"isOn": domain.IsOn,
}
this.Show()
}
func (this *UpdatePopupAction) RunPost(params struct {
DomainId int64
Name string
IsOn bool
Must *actions.Must
CSRF *actionutils.CSRF
}) {
// TODO 检查DomainId
// 记录日志
defer this.CreateLogInfo(codes.DNS_LogUpdateDomain, params.DomainId)
params.Must.
Field("name", params.Name).
Require("请输入域名")
// 校验域名
domain := strings.ToLower(params.Name)
domain = strings.Replace(domain, " ", "", -1)
if !domainutils.ValidateDomainFormat(domain) {
this.Fail("域名格式不正确,请修改后重新提交")
}
_, err := this.RPC().DNSDomainRPC().UpdateDNSDomain(this.UserContext(), &pb.UpdateDNSDomainRequest{
DnsDomainId: params.DomainId,
Name: domain,
IsOn: params.IsOn,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,17 @@
package dns
import (
"github.com/iwind/TeaGo/actions"
"net/http"
)
type Helper struct {
}
func (this *Helper) BeforeAction(action *actions.ActionObject) {
if action.Request.Method != http.MethodGet {
return
}
action.Data["teaMenu"] = "dns"
}

View File

@@ -0,0 +1,90 @@
package dns
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("dns", "dns", "")
}
func (this *IndexAction) RunGet(params struct {
Keyword string
}) {
//用户端没有集群列表 暂时从定向到dns服务商
this.RedirectURL("/dns/providers")
return
this.Data["keyword"] = params.Keyword
countResp, err := this.RPC().NodeClusterRPC().CountAllEnabledNodeClusters(this.UserContext(), &pb.CountAllEnabledNodeClustersRequest{
Keyword: params.Keyword,
})
if err != nil {
this.ErrorPage(err)
return
}
page := this.NewPage(countResp.Count)
this.Data["page"] = page.AsHTML()
clustersResp, err := this.RPC().NodeClusterRPC().ListEnabledNodeClusters(this.UserContext(), &pb.ListEnabledNodeClustersRequest{
Keyword: params.Keyword,
Offset: page.Offset,
Size: page.Size,
})
if err != nil {
this.ErrorPage(err)
return
}
clusterMaps := []maps.Map{}
for _, cluster := range clustersResp.NodeClusters {
domainId := cluster.DnsDomainId
domainName := ""
providerId := int64(0)
providerName := ""
providerTypeName := ""
if cluster.DnsDomainId > 0 {
domainResp, err := this.RPC().DNSDomainRPC().FindBasicDNSDomain(this.UserContext(), &pb.FindBasicDNSDomainRequest{DnsDomainId: domainId})
if err != nil {
this.ErrorPage(err)
return
}
domain := domainResp.DnsDomain
if domain == nil {
domainId = 0
} else {
domainName = domain.Name
providerResp, err := this.RPC().DNSProviderRPC().FindEnabledDNSProvider(this.UserContext(), &pb.FindEnabledDNSProviderRequest{DnsProviderId: domain.ProviderId})
if err != nil {
this.ErrorPage(err)
return
}
if providerResp.DnsProvider != nil {
providerId = providerResp.DnsProvider.Id
providerName = providerResp.DnsProvider.Name
providerTypeName = providerResp.DnsProvider.TypeName
}
}
}
clusterMaps = append(clusterMaps, maps.Map{
"id": cluster.Id,
"name": cluster.Name,
"dnsName": cluster.DnsName,
"domainId": domainId,
"domainName": domainName,
"providerId": providerId,
"providerName": providerName,
"providerTypeName": providerTypeName,
})
}
this.Data["clusters"] = clusterMaps
this.Show()
}

View File

@@ -0,0 +1,51 @@
package dns
import (
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/dns/domains"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/dns/providers"
"github.com/TeaOSLab/EdgeUser/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth("")).
Helper(new(Helper)).
Prefix("/dns").
Get("", new(IndexAction)).
Post("/providerOptions", new(ProviderOptionsAction)).
Post("/domainOptions", new(DomainOptionsAction)).
// 服务商
Prefix("/dns/providers").
Data("teaMenu", "provider").
//Data("teaSubMenu", "provider").
Get("", new(providers.IndexAction)).
GetPost("/createPopup", new(providers.CreatePopupAction)).
GetPost("/updatePopup", new(providers.UpdatePopupAction)).
Post("/delete", new(providers.DeleteAction)).
Get("/provider", new(providers.ProviderAction)).
Post("/syncDomains", new(providers.SyncDomainsAction)).
EndData().
// 域名
Prefix("/dns/domains").
Data("teaSubMenu", "provider").
GetPost("/createPopup", new(domains.CreatePopupAction)).
GetPost("/updatePopup", new(domains.UpdatePopupAction)).
Post("/delete", new(domains.DeleteAction)).
Post("/recover", new(domains.RecoverAction)).
Post("/sync", new(domains.SyncAction)).
Get("/routesPopup", new(domains.RoutesPopupAction)).
GetPost("/selectPopup", new(domains.SelectPopupAction)).
Get("/clustersPopup", new(domains.ClustersPopupAction)).
Get("/nodesPopup", new(domains.NodesPopupAction)).
Get("/serversPopup", new(domains.ServersPopupAction)).
EndData().
//
EndAll()
})
}

View File

@@ -0,0 +1,32 @@
package dns
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
)
// 服务商选项
type ProviderOptionsAction struct {
actionutils.ParentAction
}
func (this *ProviderOptionsAction) RunPost(params struct {
Type string
}) {
providersResp, err := this.RPC().DNSProviderRPC().FindAllEnabledDNSProvidersWithType(this.UserContext(), &pb.FindAllEnabledDNSProvidersWithTypeRequest{ProviderTypeCode: params.Type})
if err != nil {
this.ErrorPage(err)
return
}
providerMaps := []maps.Map{}
for _, provider := range providersResp.DnsProviders {
providerMaps = append(providerMaps, maps.Map{
"id": provider.Id,
"name": provider.Name,
})
}
this.Data["providers"] = providerMaps
this.Success()
}

View File

@@ -0,0 +1,353 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package providers
import (
"regexp"
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/rands"
"github.com/iwind/TeaGo/types"
)
type CreatePopupAction struct {
actionutils.ParentAction
}
func (this *CreatePopupAction) Init() {
this.Nav("", "", "")
}
func (this *CreatePopupAction) RunGet(params struct{}) {
// 所有厂商
typesResp, err := this.RPC().DNSProviderRPC().FindAllDNSProviderTypes(this.UserContext(), &pb.FindAllDNSProviderTypesRequest{})
if err != nil {
this.ErrorPage(err)
return
}
typeMaps := []maps.Map{}
for _, t := range typesResp.ProviderTypes {
typeMaps = append(typeMaps, maps.Map{
"name": t.Name,
"code": t.Code,
"description": t.Description,
})
}
this.Data["types"] = typeMaps
// 自动生成CustomHTTP私钥
this.Data["paramCustomHTTPSecret"] = rands.HexString(32)
// EdgeDNS集群列表
nsClustersResp, err := this.RPC().NSClusterRPC().FindAllNSClusters(this.UserContext(), &pb.FindAllNSClustersRequest{})
if err != nil {
this.ErrorPage(err)
return
}
var nsClusterMaps = []maps.Map{}
for _, nsCluster := range nsClustersResp.NsClusters {
nsClusterMaps = append(nsClusterMaps, maps.Map{
"id": nsCluster.Id,
"name": nsCluster.Name,
})
}
this.Data["nsClusters"] = nsClusterMaps
this.Show()
}
func (this *CreatePopupAction) RunPost(params struct {
Name string
Type string
// DNSPod
ParamDNSPodId string
ParamDNSPodToken string
ParamDNSPodRegion string
ParamDNSPodAPIType string
ParamDNSPodAccessKeyId string
ParamDNSPodAccessKeySecret string
// AliDNS
ParamAliDNSAccessKeyId string
ParamAliDNSAccessKeySecret string
ParamAliDNSRegionId string
// HuaweiDNS
ParamHuaweiAccessKeyId string
ParamHuaweiAccessKeySecret string
ParamHuaweiEndpoint string
// CloudFlare
ParamCloudFlareAPIKey string
ParamCloudFlareEmail string
// GoDaddy
ParamGoDaddyKey string
ParamGoDaddySecret string
// ClouDNS
ParamClouDNSAuthId string
ParamClouDNSSubAuthId string
ParamClouDNSAuthPassword string
// DNS.COM
ParamDNSComKey string
ParamDNSComSecret string
// DNS.LA
ParamDNSLaAPIId string
ParamDNSLaSecret string
// VolcEngine
ParamVolcEngineAccessKeyId string
ParamVolcEngineAccessKeySecret string
// Amazon Route 53
ParamAmazonRoute53AccessKeyId string
ParamAmazonRoute53AccessKeySecret string
ParamAmazonRoute53Region string
// Microsoft Azure DNS
ParamAzureDNSSubscriptionId string
ParamAzureDNSTenantId string
ParamAzureDNSClientId string
ParamAzureDNSClientSecret string
ParamAzureDNSResourceGroupName string
// bunny.net
ParamBunnyNetAPIKey string
// Gname
ParamGnameAppid string
ParamGnameSecret string
// Local EdgeDNS
ParamLocalEdgeDNSClusterId int64
// EdgeDNS API
ParamEdgeDNSAPIHost string
ParamEdgeDNSAPIRole string
ParamEdgeDNSAPIAccessKeyId string
ParamEdgeDNSAPIAccessKeySecret string
// CustomHTTP
ParamCustomHTTPURL string
ParamCustomHTTPSecret string
MinTTL int32
Must *actions.Must
CSRF *actionutils.CSRF
}) {
params.Must.
Field("name", params.Name).
Require("请输入账号说明").
Field("type", params.Type).
Require("请选择服务商厂家")
var apiParams = maps.Map{}
switch params.Type {
case "dnspod":
apiParams["apiType"] = params.ParamDNSPodAPIType
switch params.ParamDNSPodAPIType {
case "tencentDNS":
params.Must.
Field("paramDNSPodAccessKeyId", params.ParamDNSPodAccessKeyId).
Require("请输入SecretId").
Field("paramDNSPodAccessKeySecret", params.ParamDNSPodAccessKeySecret).
Require("请输入SecretKey")
apiParams["accessKeyId"] = params.ParamDNSPodAccessKeyId
apiParams["accessKeySecret"] = params.ParamDNSPodAccessKeySecret
apiParams["region"] = params.ParamDNSPodRegion
default:
params.Must.
Field("paramId", params.ParamDNSPodId).
Require("请输入密钥ID").
Field("paramToken", params.ParamDNSPodToken).
Require("请输入密钥Token")
apiParams["id"] = params.ParamDNSPodId
apiParams["token"] = params.ParamDNSPodToken
apiParams["region"] = params.ParamDNSPodRegion
}
case "alidns":
params.Must.
Field("paramAliDNSAccessKeyId", params.ParamAliDNSAccessKeyId).
Require("请输入AccessKeyId").
Field("paramAliDNSAccessKeySecret", params.ParamAliDNSAccessKeySecret).
Require("请输入AccessKeySecret")
apiParams["accessKeyId"] = params.ParamAliDNSAccessKeyId
apiParams["accessKeySecret"] = params.ParamAliDNSAccessKeySecret
apiParams["regionId"] = params.ParamAliDNSRegionId
case "huaweiDNS":
params.Must.
Field("paramHuaweiAccessKeyId", params.ParamHuaweiAccessKeyId).
Require("请输入AccessKeyId").
Field("paramHuaweiAccessKeySecret", params.ParamHuaweiAccessKeySecret).
Require("请输入AccessKeySecret")
apiParams["accessKeyId"] = params.ParamHuaweiAccessKeyId
apiParams["accessKeySecret"] = params.ParamHuaweiAccessKeySecret
apiParams["endpoint"] = params.ParamHuaweiEndpoint
case "cloudFlare":
params.Must.
Field("paramCloudFlareAPIKey", params.ParamCloudFlareAPIKey).
Require("请输入API密钥").
Field("paramCloudFlareEmail", params.ParamCloudFlareEmail).
Email("请输入正确格式的邮箱地址")
apiParams["apiKey"] = params.ParamCloudFlareAPIKey
apiParams["email"] = params.ParamCloudFlareEmail
case "godaddy":
params.Must.
Field("paramGodaddyKey", params.ParamGoDaddyKey).
Require("请输入Key").
Field("paramGodaddySecret", params.ParamGoDaddySecret).
Require("请输入Secret")
apiParams["key"] = params.ParamGoDaddyKey
apiParams["secret"] = params.ParamGoDaddySecret
case "cloudns":
var authIdString = params.ParamClouDNSAuthId
var subAuthIdString = params.ParamClouDNSSubAuthId
var authPassword = params.ParamClouDNSAuthPassword
if len(authIdString) == 0 && len(subAuthIdString) == 0 {
this.FailField("paramClouDNSAuthId", "请输入用户或者子用户的认证IDauth-id")
}
if len(authIdString) > 0 {
if !regexp.MustCompile(`^\d+$`).MatchString(authIdString) {
this.FailField("paramClouDNSAuthId", "用户认证ID需要是一个整数")
}
}
if len(subAuthIdString) > 0 {
if !regexp.MustCompile(`^\d+$`).MatchString(subAuthIdString) {
this.FailField("paramClouDNSSubAuthId", "子用户认证ID需要是一个整数")
}
}
if len(authPassword) == 0 {
this.FailField("paramClouDNSPassword", "请输入用户或者子用户的认证密码")
}
apiParams["authId"] = types.Int64(authIdString)
apiParams["subAuthId"] = types.Int64(subAuthIdString)
apiParams["authPassword"] = authPassword
case "dnscom":
params.Must.
Field("paramDNSComKey", params.ParamDNSComKey).
Require("请输入ApiKey").
Field("paramDNSComSecret", params.ParamDNSComSecret).
Require("请输入ApiSecret")
apiParams["key"] = params.ParamDNSComKey
apiParams["secret"] = params.ParamDNSComSecret
case "dnsla":
params.Must.
Field("paramDNSLaAPIId", params.ParamDNSLaAPIId).
Require("请输入API ID").
Field("paramDNSLaSecret", params.ParamDNSLaSecret).
Require("请输入API密钥")
apiParams["apiId"] = params.ParamDNSLaAPIId
apiParams["secret"] = params.ParamDNSLaSecret
case "volcEngine":
params.Must.
Field("paramVolcEngineAccessKeyId", params.ParamVolcEngineAccessKeyId).
Require("请输入Access Key ID").
Field("paramVolcEngineAccessKeySecret", params.ParamVolcEngineAccessKeySecret).
Require("请输入Secret Access Key")
apiParams["accessKeyId"] = params.ParamVolcEngineAccessKeyId
apiParams["accessKeySecret"] = params.ParamVolcEngineAccessKeySecret
case "amazonRoute53":
params.Must.
Field("paramAmazonRoute53AccessKeyId", params.ParamAmazonRoute53AccessKeyId).
Require("请输入Access Key ID").
Field("paramAmazonRoute53AccessKeySecret", params.ParamAmazonRoute53AccessKeySecret).
Require("请输入Secret Access Key")
apiParams["accessKeyId"] = params.ParamAmazonRoute53AccessKeyId
apiParams["accessKeySecret"] = params.ParamAmazonRoute53AccessKeySecret
apiParams["region"] = params.ParamAmazonRoute53Region
case "azureDNS":
params.Must.
Field("paramAzureDNSSubscriptionId", params.ParamAzureDNSSubscriptionId).
Require("请输入Subscription ID").
Field("paramAzureDNSTenantId", params.ParamAzureDNSTenantId).
Require("请输入Tenant ID").
Field("paramAzureDNSClientId", params.ParamAzureDNSClientId).
Require("请输入Client ID").
Field("paramAzureDNSClientSecret", params.ParamAzureDNSClientSecret).
Require("请输入Client Secret").
Field("paramAzureDNSResourceGroupName", params.ParamAzureDNSResourceGroupName).
Require("请输入Resource Group Name")
apiParams["subscriptionId"] = params.ParamAzureDNSSubscriptionId
apiParams["tenantId"] = params.ParamAzureDNSTenantId
apiParams["clientId"] = params.ParamAzureDNSClientId
apiParams["clientSecret"] = params.ParamAzureDNSClientSecret
apiParams["resourceGroupName"] = params.ParamAzureDNSResourceGroupName
case "bunnyNet":
params.Must.
Field("paramBunnyNetAPIKey", params.ParamBunnyNetAPIKey).
Require("请输入API密钥")
apiParams["apiKey"] = params.ParamBunnyNetAPIKey
case "gname":
params.Must.
Field("paramGnameAppid", params.ParamGnameAppid).
Require("请输入APPID").
Field("paramGnameSecret", params.ParamGnameSecret).
Require("请输入API Secret")
apiParams["appid"] = params.ParamGnameAppid
apiParams["secret"] = params.ParamGnameSecret
case "localEdgeDNS":
params.Must.
Field("paramLocalEdgeDNSClusterId", params.ParamLocalEdgeDNSClusterId).
Gt(0, "请选择域名服务集群")
apiParams["clusterId"] = params.ParamLocalEdgeDNSClusterId
case "edgeDNSAPI":
params.Must.
Field("paramEdgeDNSAPIHost", params.ParamEdgeDNSAPIHost).
Require("请输入API地址").
Field("paramEdgeDNSAPIRole", params.ParamEdgeDNSAPIRole).
Require("请选择AccessKey类型").
Field("paramEdgeDNSAPIAccessKeyId", params.ParamEdgeDNSAPIAccessKeyId).
Require("请输入AccessKey ID").
Field("paramEdgeDNSAPIAccessKeySecret", params.ParamEdgeDNSAPIAccessKeySecret).
Require("请输入AccessKey密钥")
apiParams["host"] = params.ParamEdgeDNSAPIHost
apiParams["role"] = params.ParamEdgeDNSAPIRole
apiParams["accessKeyId"] = params.ParamEdgeDNSAPIAccessKeyId
apiParams["accessKeySecret"] = params.ParamEdgeDNSAPIAccessKeySecret
case "customHTTP":
params.Must.
Field("paramCustomHTTPURL", params.ParamCustomHTTPURL).
Require("请输入HTTP URL").
Match("^(?i)(http|https):", "URL必须以http://或者https://开头").
Field("paramCustomHTTPSecret", params.ParamCustomHTTPSecret).
Require("请输入私钥")
apiParams["url"] = params.ParamCustomHTTPURL
apiParams["secret"] = params.ParamCustomHTTPSecret
default:
this.Fail("暂时不支持此服务商'" + params.Type + "'")
}
createResp, err := this.RPC().DNSProviderRPC().CreateDNSProvider(this.UserContext(), &pb.CreateDNSProviderRequest{
Name: params.Name,
Type: params.Type,
ApiParamsJSON: apiParams.AsJSON(),
MinTTL: params.MinTTL,
})
if err != nil {
this.ErrorPage(err)
return
}
defer this.CreateLogInfo(codes.DNSProvider_LogCreateDNSProvider, createResp.DnsProviderId)
this.Success()
}

View File

@@ -0,0 +1,50 @@
package providers
import (
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/utils/numberutils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
)
type DeleteAction struct {
actionutils.ParentAction
}
func (this *DeleteAction) RunPost(params struct {
ProviderId int64
}) {
// TODO 检查权限
// 记录日志
defer this.CreateLogInfo(codes.DNSProvider_LogDeleteDNSProvider, params.ProviderId)
// 检查是否被集群使用
countClustersResp, err := this.RPC().NodeClusterRPC().CountAllEnabledNodeClustersWithDNSProviderId(this.UserContext(), &pb.CountAllEnabledNodeClustersWithDNSProviderIdRequest{DnsProviderId: params.ProviderId})
if err != nil {
this.ErrorPage(err)
return
}
if countClustersResp.Count > 0 {
this.Fail("当前DNS服务商账号正在被" + numberutils.FormatInt64(countClustersResp.Count) + "个集群所使用,所以不能删除。请修改集群设置后再操作。")
}
// 判断是否被ACME任务使用
countACMETasksResp, err := this.RPC().ACMETaskRPC().CountEnabledACMETasksWithDNSProviderId(this.UserContext(), &pb.CountEnabledACMETasksWithDNSProviderIdRequest{DnsProviderId: params.ProviderId})
if err != nil {
this.ErrorPage(err)
return
}
if countACMETasksResp.Count > 0 {
this.Fail("当前DNS服务商账号正在被" + numberutils.FormatInt64(countACMETasksResp.Count) + "个ACME证书申请任务使用所以不能删除。请修改ACME证书申请任务后再操作。")
}
// 执行删除
_, err = this.RPC().DNSProviderRPC().DeleteDNSProvider(this.UserContext(), &pb.DeleteDNSProviderRequest{DnsProviderId: params.ProviderId})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,113 @@
package providers
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
timeutil "github.com/iwind/TeaGo/utils/time"
"regexp"
"strings"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "", "provider")
}
func (this *IndexAction) RunGet(params struct {
Keyword string
Domain string
ProviderType string
}) {
this.Data["keyword"] = params.Keyword
this.Data["domain"] = params.Domain
this.Data["providerType"] = params.ProviderType
// 格式化域名
var domain = regexp.MustCompile(`^(www\.)`).ReplaceAllString(params.Domain, "")
domain = strings.ToLower(domain)
countResp, err := this.RPC().DNSProviderRPC().CountAllEnabledDNSProviders(this.UserContext(), &pb.CountAllEnabledDNSProvidersRequest{
UserId: this.UserId(),
Keyword: params.Keyword,
Domain: domain,
Type: params.ProviderType,
})
if err != nil {
this.ErrorPage(err)
return
}
count := countResp.Count
page := this.NewPage(count)
this.Data["page"] = page.AsHTML()
providersResp, err := this.RPC().DNSProviderRPC().ListEnabledDNSProviders(this.UserContext(), &pb.ListEnabledDNSProvidersRequest{
UserId: this.UserId(),
Keyword: params.Keyword,
Domain: domain,
Type: params.ProviderType,
Offset: page.Offset,
Size: page.Size,
})
if err != nil {
this.ErrorPage(err)
return
}
var providerMaps = []maps.Map{}
for _, provider := range providersResp.DnsProviders {
dataUpdatedTime := ""
if provider.DataUpdatedAt > 0 {
dataUpdatedTime = timeutil.FormatTime("Y-m-d H:i:s", provider.DataUpdatedAt)
}
// 域名
countDomainsResp, err := this.RPC().DNSDomainRPC().CountAllDNSDomainsWithDNSProviderId(this.UserContext(), &pb.CountAllDNSDomainsWithDNSProviderIdRequest{
DnsProviderId: provider.Id,
})
if err != nil {
this.ErrorPage(err)
return
}
countDomains := countDomainsResp.Count
providerMaps = append(providerMaps, maps.Map{
"id": provider.Id,
"name": provider.Name,
"type": provider.Type,
"typeName": provider.TypeName,
"dataUpdatedTime": dataUpdatedTime,
"countDomains": countDomains,
})
}
this.Data["providers"] = providerMaps
// 类型
typesResponse, err := this.RPC().DNSProviderRPC().FindAllDNSProviderTypes(this.UserContext(), &pb.FindAllDNSProviderTypesRequest{})
if err != nil {
this.ErrorPage(err)
return
}
var providerTypeMaps = []maps.Map{}
for _, providerType := range typesResponse.ProviderTypes {
countProvidersWithTypeResp, err := this.RPC().DNSProviderRPC().CountAllEnabledDNSProviders(this.UserContext(), &pb.CountAllEnabledDNSProvidersRequest{
Type: providerType.Code,
})
if err != nil {
this.ErrorPage(err)
return
}
if countProvidersWithTypeResp.Count > 0 {
providerTypeMaps = append(providerTypeMaps, maps.Map{
"name": providerType.Name,
"code": providerType.Code,
"count": countProvidersWithTypeResp.Count,
})
}
}
this.Data["providerTypes"] = providerTypeMaps
this.Show()
}

View File

@@ -0,0 +1,116 @@
package providers
import (
"encoding/json"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
timeutil "github.com/iwind/TeaGo/utils/time"
)
type ProviderAction struct {
actionutils.ParentAction
}
func (this *ProviderAction) Init() {
this.Nav("", "", "")
}
func (this *ProviderAction) RunGet(params struct {
ProviderId int64
Page int
Filter string
}) {
this.Data["pageNo"] = params.Page
this.Data["filter"] = params.Filter
providerResp, err := this.RPC().DNSProviderRPC().FindEnabledDNSProvider(this.UserContext(), &pb.FindEnabledDNSProviderRequest{DnsProviderId: params.ProviderId})
if err != nil {
this.ErrorPage(err)
return
}
provider := providerResp.DnsProvider
if provider == nil {
this.NotFound("dnsProvider", params.ProviderId)
return
}
apiParams := maps.Map{}
if len(provider.ApiParamsJSON) > 0 {
err = json.Unmarshal(provider.ApiParamsJSON, &apiParams)
if err != nil {
this.ErrorPage(err)
return
}
}
// 本地EdgeDNS相关
localEdgeDNSMap, err := this.readEdgeDNS(provider, apiParams)
if err != nil {
this.ErrorPage(err)
return
}
this.Data["provider"] = maps.Map{
"id": provider.Id,
"name": provider.Name,
"type": provider.Type,
"typeName": provider.TypeName,
"apiParams": apiParams,
"localEdgeDNS": localEdgeDNSMap,
"minTTL": provider.MinTTL,
}
// 域名数量
countDomainsResp, err := this.RPC().DNSDomainRPC().CountAllDNSDomainsWithDNSProviderId(this.UserContext(), &pb.CountAllDNSDomainsWithDNSProviderIdRequest{
DnsProviderId: params.ProviderId,
IsDeleted: params.Filter == "deleted",
IsDown: params.Filter == "down",
})
if err != nil {
this.ErrorPage(err)
return
}
var countDomains = countDomainsResp.Count
var page = this.NewPage(countDomains)
this.Data["page"] = page.AsHTML()
// 域名
domainsResp, err := this.RPC().DNSDomainRPC().ListBasicDNSDomainsWithDNSProviderId(this.UserContext(), &pb.ListBasicDNSDomainsWithDNSProviderIdRequest{
DnsProviderId: provider.Id,
IsDeleted: params.Filter == "deleted",
IsDown: params.Filter == "down",
Offset: page.Offset,
Size: page.Size,
})
if err != nil {
this.ErrorPage(err)
return
}
domainMaps := []maps.Map{}
for _, domain := range domainsResp.DnsDomains {
dataUpdatedTime := ""
if domain.DataUpdatedAt > 0 {
dataUpdatedTime = timeutil.FormatTime("Y-m-d H:i:s", domain.DataUpdatedAt)
}
domainMaps = append(domainMaps, maps.Map{
"id": domain.Id,
"name": domain.Name,
"isOn": domain.IsOn,
"isUp": domain.IsUp,
"isDeleted": domain.IsDeleted,
"dataUpdatedTime": dataUpdatedTime,
"countRoutes": len(domain.Routes),
"countServerRecords": domain.CountServerRecords,
"serversChanged": domain.ServersChanged,
"countNodeRecords": domain.CountNodeRecords,
"nodesChanged": domain.NodesChanged,
"countClusters": domain.CountNodeClusters,
"countAllNodes": domain.CountAllNodes,
"countAllServers": domain.CountAllServers,
})
}
this.Data["domains"] = domainMaps
this.Show()
}

View File

@@ -0,0 +1,29 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package providers
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
)
func (this *ProviderAction) readEdgeDNS(provider *pb.DNSProvider, apiParams maps.Map) (maps.Map, error) {
var localEdgeDNSMap = maps.Map{}
if provider.Type == "localEdgeDNS" {
nsClusterId := apiParams.GetInt64("clusterId")
nsClusterResp, err := this.RPC().NSClusterRPC().FindNSCluster(this.UserContext(), &pb.FindNSClusterRequest{NsClusterId: nsClusterId})
if err != nil {
return nil, err
}
nsCluster := nsClusterResp.NsCluster
if nsCluster != nil {
localEdgeDNSMap = maps.Map{
"id": nsCluster.Id,
"name": nsCluster.Name,
}
}
}
return localEdgeDNSMap, nil
}

View File

@@ -0,0 +1,25 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package providers
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
)
type SyncDomainsAction struct {
actionutils.ParentAction
}
func (this *SyncDomainsAction) RunPost(params struct {
ProviderId int64
}) {
resp, err := this.RPC().DNSDomainRPC().SyncDNSDomainsFromProvider(this.UserContext(), &pb.SyncDNSDomainsFromProviderRequest{DnsProviderId: params.ProviderId})
if err != nil {
this.Fail("更新域名失败:" + err.Error())
}
this.Data["hasChanges"] = resp.HasChanges
this.Success()
}

View File

@@ -0,0 +1,384 @@
package providers
import (
"encoding/json"
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
"regexp"
)
type UpdatePopupAction struct {
actionutils.ParentAction
}
func (this *UpdatePopupAction) Init() {
this.Nav("", "", "")
}
func (this *UpdatePopupAction) RunGet(params struct {
ProviderId int64
}) {
providerResp, err := this.RPC().DNSProviderRPC().FindEnabledDNSProvider(this.UserContext(), &pb.FindEnabledDNSProviderRequest{DnsProviderId: params.ProviderId})
if err != nil {
this.ErrorPage(err)
return
}
var provider = providerResp.DnsProvider
if provider == nil {
this.NotFound("dnsProvider", params.ProviderId)
return
}
var apiParams = maps.Map{}
if len(provider.ApiParamsJSON) > 0 {
err = json.Unmarshal(provider.ApiParamsJSON, &apiParams)
if err != nil {
this.ErrorPage(err)
return
}
}
this.Data["provider"] = maps.Map{
"id": provider.Id,
"name": provider.Name,
"type": provider.Type,
"typeName": provider.TypeName,
"minTTL": provider.MinTTL,
"params": apiParams,
}
// 所有厂商
typesResp, err := this.RPC().DNSProviderRPC().FindAllDNSProviderTypes(this.UserContext(), &pb.FindAllDNSProviderTypesRequest{})
if err != nil {
this.ErrorPage(err)
return
}
var typeMaps = []maps.Map{}
for _, t := range typesResp.ProviderTypes {
typeMaps = append(typeMaps, maps.Map{
"name": t.Name,
"code": t.Code,
"description": t.Description,
})
}
this.Data["types"] = typeMaps
// EdgeDNS集群列表
nsClustersResp, err := this.RPC().NSClusterRPC().FindAllNSClusters(this.UserContext(), &pb.FindAllNSClustersRequest{})
if err != nil {
this.ErrorPage(err)
return
}
var nsClusterMaps = []maps.Map{}
for _, nsCluster := range nsClustersResp.NsClusters {
nsClusterMaps = append(nsClusterMaps, maps.Map{
"id": nsCluster.Id,
"name": nsCluster.Name,
})
}
this.Data["nsClusters"] = nsClusterMaps
this.Show()
}
func (this *UpdatePopupAction) RunPost(params struct {
ProviderId int64
Name string
Type string
// DNSPod
ParamDNSPodId string
ParamDNSPodToken string
ParamDNSPodRegion string
ParamDNSPodAPIType string
ParamDNSPodAccessKeyId string
ParamDNSPodAccessKeySecret string
// AliDNS
ParamAliDNSAccessKeyId string
ParamAliDNSAccessKeySecret string
ParamAliDNSRegionId string
// HuaweiDNS
ParamHuaweiAccessKeyId string
ParamHuaweiAccessKeySecret string
ParamHuaweiEndpoint string
// DNS.COM
ParamApiKey string
ParamApiSecret string
// CloudFlare
ParamCloudFlareAPIKey string
ParamCloudFlareEmail string
// GoDaddy
ParamGoDaddyKey string
ParamGoDaddySecret string
// ClouDNS
ParamClouDNSAuthId string
ParamClouDNSSubAuthId string
ParamClouDNSAuthPassword string
// DNS.COM
ParamDNSComKey string
ParamDNSComSecret string
// DNS.LA
ParamDNSLaAPIId string
ParamDNSLaSecret string
// VolcEngine
ParamVolcEngineAccessKeyId string
ParamVolcEngineAccessKeySecret string
// Amazon Route 53
ParamAmazonRoute53AccessKeyId string
ParamAmazonRoute53AccessKeySecret string
ParamAmazonRoute53Region string
// Microsoft Azure DNS
ParamAzureDNSSubscriptionId string
ParamAzureDNSTenantId string
ParamAzureDNSClientId string
ParamAzureDNSClientSecret string
ParamAzureDNSResourceGroupName string
// bunny.net
ParamBunnyNetAPIKey string
// Gname
ParamGnameAppid string
ParamGnameSecret string
// Local EdgeDNS
ParamLocalEdgeDNSClusterId int64
// EdgeDNS API
ParamEdgeDNSAPIHost string
ParamEdgeDNSAPIRole string
ParamEdgeDNSAPIAccessKeyId string
ParamEdgeDNSAPIAccessKeySecret string
// CustomHTTP
ParamCustomHTTPURL string
ParamCustomHTTPSecret string
MinTTL int32
Must *actions.Must
CSRF *actionutils.CSRF
}) {
defer this.CreateLogInfo(codes.DNSProvider_LogUpdateDNSProvider, params.ProviderId)
params.Must.
Field("name", params.Name).
Require("请输入账号说明").
Field("type", params.Type).
Require("请选择服务商厂家")
var apiParams = maps.Map{}
switch params.Type {
case "dnspod":
apiParams["apiType"] = params.ParamDNSPodAPIType
switch params.ParamDNSPodAPIType {
case "tencentDNS":
params.Must.
Field("paramDNSPodAccessKeyId", params.ParamDNSPodAccessKeyId).
Require("请输入SecretId").
Field("paramDNSPodAccessKeySecret", params.ParamDNSPodAccessKeySecret).
Require("请输入SecretKey")
apiParams["accessKeyId"] = params.ParamDNSPodAccessKeyId
apiParams["accessKeySecret"] = params.ParamDNSPodAccessKeySecret
apiParams["region"] = params.ParamDNSPodRegion
default:
params.Must.
Field("paramId", params.ParamDNSPodId).
Require("请输入密钥ID").
Field("paramToken", params.ParamDNSPodToken).
Require("请输入密钥Token")
apiParams["id"] = params.ParamDNSPodId
apiParams["token"] = params.ParamDNSPodToken
apiParams["region"] = params.ParamDNSPodRegion
}
case "alidns":
params.Must.
Field("paramAliDNSAccessKeyId", params.ParamAliDNSAccessKeyId).
Require("请输入AccessKeyId").
Field("paramAliDNSAccessKeySecret", params.ParamAliDNSAccessKeySecret).
Require("请输入AccessKeySecret")
apiParams["accessKeyId"] = params.ParamAliDNSAccessKeyId
apiParams["accessKeySecret"] = params.ParamAliDNSAccessKeySecret
apiParams["regionId"] = params.ParamAliDNSRegionId
case "huaweiDNS":
params.Must.
Field("paramHuaweiAccessKeyId", params.ParamHuaweiAccessKeyId).
Require("请输入AccessKeyId").
Field("paramHuaweiAccessKeySecret", params.ParamHuaweiAccessKeySecret).
Require("请输入AccessKeySecret")
apiParams["accessKeyId"] = params.ParamHuaweiAccessKeyId
apiParams["accessKeySecret"] = params.ParamHuaweiAccessKeySecret
apiParams["endpoint"] = params.ParamHuaweiEndpoint
case "cloudFlare":
params.Must.
Field("paramCloudFlareAPIKey", params.ParamCloudFlareAPIKey).
Require("请输入API密钥").
Field("paramCloudFlareEmail", params.ParamCloudFlareEmail).
Email("请输入正确格式的邮箱地址")
apiParams["apiKey"] = params.ParamCloudFlareAPIKey
apiParams["email"] = params.ParamCloudFlareEmail
case "godaddy":
params.Must.
Field("paramGodaddyKey", params.ParamGoDaddyKey).
Require("请输入Key").
Field("paramGodaddySecret", params.ParamGoDaddySecret).
Require("请输入Secret")
apiParams["key"] = params.ParamGoDaddyKey
apiParams["secret"] = params.ParamGoDaddySecret
case "cloudns":
var authIdString = params.ParamClouDNSAuthId
var subAuthIdString = params.ParamClouDNSSubAuthId
var authPassword = params.ParamClouDNSAuthPassword
if len(authIdString) == 0 && len(subAuthIdString) == 0 {
this.FailField("paramClouDNSAuthId", "请输入用户或者子用户的认证IDauth-id")
}
if len(authIdString) > 0 {
if !regexp.MustCompile(`^\d+$`).MatchString(authIdString) {
this.FailField("paramClouDNSAuthId", "用户认证ID需要是一个整数")
}
}
if len(subAuthIdString) > 0 {
if !regexp.MustCompile(`^\d+$`).MatchString(subAuthIdString) {
this.FailField("paramClouDNSSubAuthId", "子用户认证ID需要是一个整数")
}
}
if len(authPassword) == 0 {
this.FailField("paramClouDNSPassword", "请输入用户或者子用户的认证密码")
}
apiParams["authId"] = types.Int64(authIdString)
apiParams["subAuthId"] = types.Int64(subAuthIdString)
apiParams["authPassword"] = authPassword
case "dnscom":
params.Must.
Field("paramDNSComKey", params.ParamDNSComKey).
Require("请输入ApiKey").
Field("paramDNSComSecret", params.ParamDNSComSecret).
Require("请输入ApiSecret")
apiParams["key"] = params.ParamDNSComKey
apiParams["secret"] = params.ParamDNSComSecret
case "dnsla":
params.Must.
Field("paramDNSLaAPIId", params.ParamDNSLaAPIId).
Require("请输入API ID").
Field("paramDNSLaSecret", params.ParamDNSLaSecret).
Require("请输入API密钥")
apiParams["apiId"] = params.ParamDNSLaAPIId
apiParams["secret"] = params.ParamDNSLaSecret
case "volcEngine":
params.Must.
Field("paramVolcEngineAccessKeyId", params.ParamVolcEngineAccessKeyId).
Require("请输入Access Key ID").
Field("paramVolcEngineAccessKeySecret", params.ParamVolcEngineAccessKeySecret).
Require("请输入Secret Access Key")
apiParams["accessKeyId"] = params.ParamVolcEngineAccessKeyId
apiParams["accessKeySecret"] = params.ParamVolcEngineAccessKeySecret
case "amazonRoute53":
params.Must.
Field("paramAmazonRoute53AccessKeyId", params.ParamAmazonRoute53AccessKeyId).
Require("请输入Access Key ID").
Field("paramAmazonRoute53AccessKeySecret", params.ParamAmazonRoute53AccessKeySecret).
Require("请输入Secret Access Key")
apiParams["accessKeyId"] = params.ParamAmazonRoute53AccessKeyId
apiParams["accessKeySecret"] = params.ParamAmazonRoute53AccessKeySecret
apiParams["region"] = params.ParamAmazonRoute53Region
case "azureDNS":
params.Must.
Field("paramAzureDNSSubscriptionId", params.ParamAzureDNSSubscriptionId).
Require("请输入Subscription ID").
Field("paramAzureDNSTenantId", params.ParamAzureDNSTenantId).
Require("请输入Tenant ID").
Field("paramAzureDNSClientId", params.ParamAzureDNSClientId).
Require("请输入Client ID").
Field("paramAzureDNSClientSecret", params.ParamAzureDNSClientSecret).
Require("请输入Client Secret").
Field("paramAzureDNSResourceGroupName", params.ParamAzureDNSResourceGroupName).
Require("请输入Resource Group Name")
apiParams["subscriptionId"] = params.ParamAzureDNSSubscriptionId
apiParams["tenantId"] = params.ParamAzureDNSTenantId
apiParams["clientId"] = params.ParamAzureDNSClientId
apiParams["clientSecret"] = params.ParamAzureDNSClientSecret
apiParams["resourceGroupName"] = params.ParamAzureDNSResourceGroupName
case "bunnyNet":
params.Must.
Field("paramBunnyNetAPIKey", params.ParamBunnyNetAPIKey).
Require("请输入API密钥")
apiParams["apiKey"] = params.ParamBunnyNetAPIKey
case "gname":
params.Must.
Field("paramGnameAppid", params.ParamGnameAppid).
Require("请输入APPID").
Field("paramGnameSecret", params.ParamGnameSecret).
Require("请输入API Secret")
apiParams["appid"] = params.ParamGnameAppid
apiParams["secret"] = params.ParamGnameSecret
case "localEdgeDNS":
params.Must.
Field("paramLocalEdgeDNSClusterId", params.ParamLocalEdgeDNSClusterId).
Gt(0, "请选择域名服务集群")
apiParams["clusterId"] = params.ParamLocalEdgeDNSClusterId
case "edgeDNSAPI":
params.Must.
Field("paramEdgeDNSAPIHost", params.ParamEdgeDNSAPIHost).
Require("请输入API地址").
Field("paramEdgeDNSAPIRole", params.ParamEdgeDNSAPIRole).
Require("请选择AccessKey类型").
Field("paramEdgeDNSAPIAccessKeyId", params.ParamEdgeDNSAPIAccessKeyId).
Require("请输入AccessKey ID").
Field("paramEdgeDNSAPIAccessKeySecret", params.ParamEdgeDNSAPIAccessKeySecret).
Require("请输入AccessKey密钥")
apiParams["host"] = params.ParamEdgeDNSAPIHost
apiParams["role"] = params.ParamEdgeDNSAPIRole
apiParams["accessKeyId"] = params.ParamEdgeDNSAPIAccessKeyId
apiParams["accessKeySecret"] = params.ParamEdgeDNSAPIAccessKeySecret
case "customHTTP":
params.Must.
Field("paramCustomHTTPURL", params.ParamCustomHTTPURL).
Require("请输入HTTP URL").
Match("^(?i)(http|https):", "URL必须以http://或者https://开头").
Field("paramCustomHTTPSecret", params.ParamCustomHTTPSecret).
Require("请输入私钥")
apiParams["url"] = params.ParamCustomHTTPURL
apiParams["secret"] = params.ParamCustomHTTPSecret
default:
this.Fail("暂时不支持此服务商'" + params.Type + "'")
}
_, err := this.RPC().DNSProviderRPC().UpdateDNSProvider(this.UserContext(), &pb.UpdateDNSProviderRequest{
DnsProviderId: params.ProviderId,
Name: params.Name,
MinTTL: params.MinTTL,
ApiParamsJSON: apiParams.AsJSON(),
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,114 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package docutils
import (
"bytes"
chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
"github.com/yuin/goldmark"
highlighting "github.com/yuin/goldmark-highlighting/v2"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
"os"
"path/filepath"
"regexp"
"sync"
)
type cacheItem struct {
ModifiedAt int64
Data []byte
}
var cacheMap = map[string]*cacheItem{} // path => *cacheItem
var cacheLocker = &sync.RWMutex{}
func ReadMarkdownFile(path string, productName string, rootURL string) ([]byte, error) {
stat, err := os.Stat(path)
if err != nil {
return nil, err
}
if stat.IsDir() {
return nil, os.ErrNotExist
}
var modifiedAt = stat.ModTime().Unix()
cacheLocker.RLock()
item, ok := cacheMap[path]
if ok && item.ModifiedAt == modifiedAt {
cacheLocker.RUnlock()
return item.Data, nil
}
cacheLocker.RUnlock()
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
// keywords
data = bytes.ReplaceAll(data, []byte("GoEdge"), []byte(productName))
data = bytes.ReplaceAll(data, []byte("GOEDGE"), []byte(productName))
data = bytes.ReplaceAll(data, []byte("http://goedge.cn"), []byte("http://example.com"))
data = bytes.ReplaceAll(data, []byte("https://goedge.cn"), []byte("https://example.com"))
var markdown = goldmark.New(
goldmark.WithParserOptions(
parser.WithAutoHeadingID(), // read note
),
goldmark.WithExtensions(extension.Table,
extension.Strikethrough,
highlighting.NewHighlighting(highlighting.WithStyle("github"), highlighting.WithFormatOptions(chromahtml.TabWidth(4))),
),
goldmark.WithRendererOptions(html.WithHardWraps()),
)
var buf = &bytes.Buffer{}
err = markdown.Convert(data, buf)
if err != nil {
return nil, err
}
data = buf.Bytes()
// convert links
{
var reg = regexp.MustCompile(`(?U)<a href="(.+\.md)">`)
data = reg.ReplaceAllFunc(data, func(link []byte) []byte {
var pieces = reg.FindSubmatch(link)
if len(pieces) > 1 {
var newLink = append([]byte(rootURL), bytes.TrimSuffix(pieces[1], []byte(".md"))...)
newLink = append(newLink, []byte(".html")...)
newLink = []byte(filepath.Clean(string(newLink)))
return bytes.ReplaceAll(link, pieces[1], newLink)
}
return link
})
}
// convert images
{
var reg = regexp.MustCompile(`(?U)<img src="(.+)"[^>]*>`)
data = reg.ReplaceAllFunc(data, func(link []byte) []byte {
var pieces = reg.FindSubmatch(link)
if len(pieces) > 1 {
var newLink = append([]byte(rootURL), pieces[1]...)
newLink = []byte(filepath.Clean(string(newLink)))
return []byte("<a href=\"" + string(newLink) + "\" target=\"_blank\">" + string(bytes.ReplaceAll(link, pieces[1], newLink)) + "</a>")
}
return link
})
}
// put into cache
item = &cacheItem{
ModifiedAt: modifiedAt,
Data: data,
}
cacheLocker.Lock()
cacheMap[path] = item
cacheLocker.Unlock()
return data, nil
}

View File

@@ -0,0 +1,304 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package docs
import (
"bytes"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/docs/docutils"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/types"
"github.com/yuin/goldmark"
"io"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
)
const URLPrefix = "/docs"
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "", "")
}
func (this *IndexAction) RunGet(params struct {
Page string
}) {
var docRoot = Tea.Root
if Tea.IsTesting() {
docRoot += "/../web"
} else {
docRoot += "/web"
}
docRoot += "/docs"
docRoot = filepath.Clean(docRoot)
var pageName = params.Page
if strings.Contains(pageName, "..") { // prevent path traveling
this.ResponseWriter.WriteHeader(http.StatusNotFound)
this.WriteString("not found '" + params.Page + "'")
return
}
if len(pageName) == 0 {
pageName = "index.html"
} else if strings.HasSuffix(pageName, "/") {
pageName += "index.html"
}
// extension
var ext = filepath.Ext(pageName)
switch ext {
case ".html":
this.doHTML(docRoot, pageName)
case ".jpg", ".jpeg", ".png", "gif", ".webp":
this.doImage(docRoot, pageName, ext)
default:
this.ResponseWriter.WriteHeader(http.StatusNotFound)
this.WriteString("not found '" + params.Page + "'")
}
}
func (this *IndexAction) doHTML(docRoot string, pageName string) {
pageName = strings.TrimSuffix(pageName, ".html")
var path = filepath.Clean(docRoot + "/" + pageName + ".md")
if !strings.HasPrefix(path, docRoot) {
this.ResponseWriter.WriteHeader(http.StatusNotFound)
this.WriteString("not found '" + pageName + "'")
return
}
// convert links
var rootURL = URLPrefix + "/"
var pageDir = filepath.Dir(pageName)
if len(pageDir) > 0 && pageDir != "." {
rootURL += pageDir + "/"
}
resultData, err := docutils.ReadMarkdownFile(path, this.Data.GetString("teaName"), rootURL)
if err != nil {
this.ResponseWriter.WriteHeader(http.StatusNotFound)
this.WriteString("not found '" + pageName + "'")
return
}
// toc
{
var reg = regexp.MustCompile(`(?U)<h(\d)\sid="(.+)">(.*)</h\d>`)
var subMatches = reg.FindAllStringSubmatch(string(resultData), -1)
var tocItem = &TOCItem{
Depth: 0,
}
var lastItem *TOCItem
for _, subMatch := range subMatches {
var depth = types.Int(subMatch[1])
var id = subMatch[2]
var title = subMatch[3]
var item = &TOCItem{
Depth: depth,
Id: id,
Title: title,
Children: []*TOCItem{},
Parent: nil,
}
if lastItem == nil {
item.Parent = tocItem
tocItem.Children = append(tocItem.Children, item)
} else if lastItem.Depth == item.Depth {
if lastItem.Parent != nil {
item.Parent = lastItem.Parent
lastItem.Parent.Children = append(lastItem.Parent.Children, item)
}
} else if lastItem.Depth < item.Depth {
item.Parent = lastItem
lastItem.Children = append(lastItem.Children, item)
} else {
var parent = lastItem.Parent
for parent != nil {
if parent.Depth == item.Depth {
if parent.Parent != nil {
item.Parent = parent.Parent
parent.Parent.Children = append(parent.Parent.Children, item)
}
break
}
parent = parent.Parent
}
}
lastItem = item
}
// reset parent
tocItem.UnsetParent()
if len(tocItem.Children) > 0 {
this.Data["toc"] = tocItem.AsHTML()
} else {
this.Data["toc"] = ""
}
// format <h1...6>
/**resultData = reg.ReplaceAllFunc(resultData, func(i []byte) []byte {
var subMatch = reg.FindSubmatch(i)
if len(subMatch) > 1 {
var depth = subMatch[1]
var idData = subMatch[2]
var title = subMatch[3]
return []byte("<a class=\"anchor\" id=\"" + string(idData) + "\">&nbsp;</a>\n<h" + string(depth) + ">" + string(title) + "</h" + string(depth) + ">")
}
return i
})**/
}
this.Data["content"] = string(resultData)
// 整体菜单
this.readMenu(docRoot)
this.Show()
}
func (this *IndexAction) doImage(docRoot string, pageName string, ext string) {
var path = filepath.Clean(docRoot + "/" + pageName)
if !strings.HasPrefix(path, docRoot) {
this.ResponseWriter.WriteHeader(http.StatusNotFound)
this.WriteString("not found '" + pageName + "'")
return
}
var mimeType = ""
switch ext {
case ".jpg", ".jpeg":
mimeType = "image/jpeg"
case ".png":
mimeType = "image/png"
case ".webp":
mimeType = "image/webp"
case ".gif":
mimeType = "image/gif"
}
if len(mimeType) == 0 {
this.ResponseWriter.WriteHeader(http.StatusNotFound)
return
}
stat, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
this.ResponseWriter.WriteHeader(http.StatusNotFound)
return
}
this.ErrorPage(err)
return
}
this.ResponseWriter.Header().Set("Content-Length", types.String(stat.Size()))
this.ResponseWriter.Header().Set("Content-Type", mimeType)
fp, err := os.Open(path)
if err != nil {
this.ErrorPage(err)
return
}
defer func() {
_ = fp.Close()
}()
_, err = io.Copy(this.ResponseWriter, fp)
if err != nil {
this.ErrorPage(err)
return
}
}
func (this *IndexAction) readMenu(docRoot string) {
var path = filepath.Clean(docRoot + "/@toc.md")
data, err := os.ReadFile(path)
if err != nil {
this.ErrorPage(err)
return
}
var markdown = goldmark.New()
var buf = &bytes.Buffer{}
err = markdown.Convert(data, buf)
if err != nil {
this.ErrorPage(err)
return
}
var resultString = buf.String()
var reg = regexp.MustCompile(`(?U)<a href="(.+)">`)
resultString = reg.ReplaceAllStringFunc(resultString, func(s string) string {
var match = reg.FindStringSubmatch(s)
if len(match) > 1 {
var link = match[1]
var isExternal = strings.HasPrefix(link, "http://") || strings.HasPrefix(link, "https://")
if !isExternal && !strings.HasPrefix(link, "/") {
link = URLPrefix + "/" + link
}
if !isExternal && strings.HasSuffix(link, ".md") {
link = strings.TrimSuffix(link, ".md") + ".html"
}
var a = `<a href="` + link + `"`
// open external url in new window
if isExternal {
a += ` target="_blank"`
}
// active
if link == this.Request.URL.Path {
a += ` class="active"`
}
return a + ">"
}
return s
})
this.Data["rootTOC"] = resultString
}
type TOCItem struct {
Depth int `json:"depth"`
Id string `json:"id"`
Title string `json:"title"`
Children []*TOCItem `json:"children"`
Parent *TOCItem `json:"-"`
}
func (this *TOCItem) UnsetParent() {
this.Parent = nil
for _, child := range this.Children {
child.UnsetParent()
}
}
func (this *TOCItem) AsHTML() string {
if len(this.Children) == 0 || this.Depth >= 2 /** only 2 levels **/ {
return "\n"
}
var space = strings.Repeat(" ", this.Depth)
var result = space + "<ul>\n"
for _, child := range this.Children {
var childSpace = strings.Repeat(" ", child.Depth)
result += childSpace + "<li><a href=\"#" + child.Id + "\">" + child.Title + "</a>\n" + child.AsHTML() + childSpace + "</li>\n"
}
result += space + "</ul>\n"
return result
}

View File

@@ -0,0 +1,21 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package docs
import (
"github.com/TeaOSLab/EdgeUser/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Prefix("/docs").
Helper(helpers.NewUserMustAuth("")).
Data("teaMenu", "docs").
Get("", new(IndexAction)).
Get("/", new(IndexAction)).
Get("/:page(.+)", new(IndexAction)).
EndAll()
})
}

View File

@@ -0,0 +1,12 @@
package email
import "github.com/iwind/TeaGo"
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Prefix("/email").
Get("/verify/:code", new(VerifyAction)).
EndAll()
})
}

View File

@@ -0,0 +1,66 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package email
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/configloaders"
teaconst "github.com/TeaOSLab/EdgeUser/internal/const"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
)
type VerifyAction struct {
actionutils.ParentAction
}
func (this *VerifyAction) Init() {
this.Nav("", "", "")
}
func (this *VerifyAction) RunGet(params struct {
Code string
}) {
if len(params.Code) == 0 || len(params.Code) > 128 /** 最长不超过128 **/ {
this.WriteString("code not found")
return
}
// 激活
resp, err := this.RPC().UserEmailVerificationRPC().VerifyUserEmail(this.UserContext(), &pb.VerifyUserEmailRequest{
Code: params.Code,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["email"] = resp.Email
if resp.UserId > 0 {
this.Data["isOk"] = true
if this.UserId() > 0 && this.UserId() != resp.UserId {
// TODO 暂时不对比当前用户是否一致
}
} else {
this.Data["isOk"] = false
this.Data["errorMessage"] = resp.ErrorMessage
}
// 界面
config, err := configloaders.LoadUIConfig()
if err != nil {
this.ErrorPage(err)
return
}
this.Data["systemName"] = config.UserSystemName
this.Data["showVersion"] = config.ShowVersion
if len(config.Version) > 0 {
this.Data["version"] = config.Version
} else {
this.Data["version"] = teaconst.Version
}
this.Data["faviconFileId"] = config.FaviconFileId
this.Show()
}

View File

@@ -0,0 +1,62 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package files
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/types"
"mime"
"path/filepath"
)
type FileAction struct {
actionutils.ParentAction
}
func (this *FileAction) Init() {
this.Nav("", "", "")
}
func (this *FileAction) RunGet(params struct {
FileId int64
}) {
fileResp, err := this.RPC().FileRPC().FindEnabledFile(this.UserContext(), &pb.FindEnabledFileRequest{FileId: params.FileId})
if err != nil {
this.ErrorPage(err)
return
}
var file = fileResp.File
if file == nil {
this.NotFound("File", params.FileId)
return
}
chunkIdsResp, err := this.RPC().FileChunkRPC().FindAllFileChunkIds(this.UserContext(), &pb.FindAllFileChunkIdsRequest{FileId: file.Id})
if err != nil {
this.ErrorPage(err)
return
}
this.AddHeader("Content-Length", types.String(file.Size))
if len(file.MimeType) > 0 {
this.AddHeader("Content-Type", file.MimeType)
} else if len(file.Filename) > 0 {
var ext = filepath.Ext(file.Filename)
var mimeType = mime.TypeByExtension(ext)
this.AddHeader("Content-Type", mimeType)
}
for _, chunkId := range chunkIdsResp.FileChunkIds {
chunkResp, err := this.RPC().FileChunkRPC().DownloadFileChunk(this.UserContext(), &pb.DownloadFileChunkRequest{FileChunkId: chunkId})
if err != nil {
this.ErrorPage(err)
return
}
if chunkResp.FileChunk == nil {
continue
}
_, _ = this.Write(chunkResp.FileChunk.Data)
}
}

View File

@@ -0,0 +1,14 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package files
import "github.com/iwind/TeaGo"
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Prefix("/files").
Get("/file", new(FileAction)).
EndAll()
})
}

View File

@@ -0,0 +1,162 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package bills
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
"github.com/TeaOSLab/EdgeUser/internal/utils/numberutils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
timeutil "github.com/iwind/TeaGo/utils/time"
)
type BillAction struct {
actionutils.ParentAction
}
func (this *BillAction) Init() {
this.Nav("", "", "index")
}
func (this *BillAction) RunGet(params struct {
Code string
}) {
userBillResp, err := this.RPC().UserBillRPC().FindUserBill(this.UserContext(), &pb.FindUserBillRequest{Code: params.Code})
if err != nil {
this.ErrorPage(err)
return
}
var bill = userBillResp.UserBill
if bill == nil {
this.NotFound("user bill", 0)
return
}
var userMap maps.Map = nil
if bill.User != nil {
userMap = maps.Map{
"id": bill.User.Id,
"fullname": bill.User.Fullname,
"username": bill.User.Username,
}
}
var month = bill.Month
var dayFrom = bill.DayFrom
var dayTo = bill.DayTo
this.Data["bill"] = maps.Map{
"id": bill.Id,
"isPaid": bill.IsPaid,
"month": month,
"dayFrom": dayFrom,
"dayTo": dayTo,
"amount": numberutils.FormatFloat(bill.Amount, 2),
"typeName": bill.TypeName,
"user": userMap,
"description": bill.Description,
"code": bill.Code,
"canPay": bill.CanPay,
"pricePeriodName": userconfigs.PricePeriodName(bill.PricePeriod),
"pricePeriod": bill.PricePeriod,
}
// 服务账单
var serverBillMaps = []maps.Map{}
if (bill.Type == "traffic" || bill.Type == "trafficAndBandwidth") && bill.User != nil {
countResp, err := this.RPC().ServerBillRPC().CountAllServerBills(this.UserContext(), &pb.CountAllServerBillsRequest{
UserId: bill.User.Id,
Month: bill.Month,
})
if err != nil {
this.ErrorPage(err)
return
}
var count = countResp.Count
var page = this.NewPage(count)
this.Data["page"] = page.AsHTML()
serverBillsResp, err := this.RPC().ServerBillRPC().ListServerBills(this.UserContext(), &pb.ListServerBillsRequest{
UserId: bill.User.Id,
Month: bill.Month,
Offset: page.Offset,
Size: page.Size,
})
if err != nil {
this.ErrorPage(err)
return
}
for _, serverBill := range serverBillsResp.ServerBills {
// server
var serverMap = maps.Map{"id": 0}
if serverBill.Server != nil {
serverMap = maps.Map{
"id": serverBill.Server.Id,
"name": serverBill.Server.Name,
}
}
// plan
var planMap = maps.Map{"id": 0}
if serverBill.Plan != nil {
planMap = maps.Map{
"id": serverBill.Plan.Id,
"name": serverBill.Plan.Name,
"priceType": serverBill.Plan.PriceType,
}
}
serverBillMaps = append(serverBillMaps, maps.Map{
"id": serverBill.Id,
"server": serverMap,
"plan": planMap,
"traffic": numberutils.FormatBytes(serverBill.TotalTrafficBytes),
"bandwidthPercentile": serverBill.BandwidthPercentile,
"bandwidthPercentileSize": numberutils.FormatBytes(serverBill.BandwidthPercentileBytes),
"createdTime": timeutil.FormatTime("Y-m-d H:i:s", serverBill.CreatedAt),
"amount": numberutils.FormatFloat(serverBill.Amount, 2),
"priceType": serverBill.PriceType,
})
}
}
this.Data["serverBills"] = serverBillMaps
// traffic bills
var trafficBillMaps = []maps.Map{}
trafficBillsResp, err := this.RPC().UserTrafficBillRPC().FindUserTrafficBills(this.UserContext(), &pb.FindUserTrafficBillsRequest{UserBillId: bill.Id})
if err != nil {
this.ErrorPage(err)
return
}
for _, trafficBill := range trafficBillsResp.UserTrafficBills {
var regionMap = maps.Map{"id": 0}
var nodeRegion = trafficBill.NodeRegion
if nodeRegion != nil {
regionMap = maps.Map{
"id": nodeRegion.Id,
"name": nodeRegion.Name,
}
} else if trafficBill.NodeRegionId > 0 {
regionMap = maps.Map{
"id": trafficBill.NodeRegionId,
"name": "[已取消]",
}
}
trafficBillMaps = append(trafficBillMaps, maps.Map{
"priceType": trafficBill.PriceType,
"priceTypeName": userconfigs.PriceTypeName(trafficBill.PriceType),
"trafficGB": numberutils.FormatBytes(int64(trafficBill.TrafficGB * (1 << 30))),
"trafficPackageGB": numberutils.FormatBytes(int64(trafficBill.TrafficPackageGB * (1 << 30))),
"bandwidthMB": numberutils.FormatBits(int64(trafficBill.BandwidthMB * (1 << 20))),
"bandwidthPercentile": trafficBill.BandwidthPercentile,
"amount": numberutils.FormatFloat(trafficBill.Amount, 2),
"pricePerUnit": trafficBill.PricePerUnit,
"region": regionMap,
})
}
this.Data["trafficBills"] = trafficBillMaps
this.Show()
}

View File

@@ -0,0 +1,115 @@
package bills
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
"github.com/TeaOSLab/EdgeUser/internal/utils/numberutils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"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"`
Month string
}) {
this.Data["paidFlag"] = params.PaidFlag
// 账号余额
accountResp, err := this.RPC().UserAccountRPC().FindEnabledUserAccountWithUserId(this.UserContext(), &pb.FindEnabledUserAccountWithUserIdRequest{
UserId: this.UserId(),
})
if err != nil {
this.ErrorPage(err)
return
}
var account = accountResp.UserAccount
if account == nil {
this.Fail("当前用户还没有账号")
}
// 需要支付的总额
unpaidAmountResp, err := this.RPC().UserBillRPC().SumUserUnpaidBills(this.UserContext(), &pb.SumUserUnpaidBillsRequest{UserId: this.UserId()})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["accountTotal"] = account.Total
this.Data["unpaidAmount"] = unpaidAmountResp.Amount
countResp, err := this.RPC().UserBillRPC().CountAllUserBills(this.UserContext(), &pb.CountAllUserBillsRequest{
PaidFlag: params.PaidFlag,
UserId: this.UserId(),
Month: params.Month,
})
if err != nil {
this.ErrorPage(err)
return
}
page := this.NewPage(countResp.Count)
this.Data["page"] = page.AsHTML()
billsResp, err := this.RPC().UserBillRPC().ListUserBills(this.UserContext(), &pb.ListUserBillsRequest{
PaidFlag: params.PaidFlag,
UserId: this.UserId(),
Month: params.Month,
Offset: page.Offset,
Size: page.Size,
})
if err != nil {
this.ErrorPage(err)
return
}
var 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,
}
}
var month = bill.Month
var dayFrom = bill.DayFrom
var dayTo = bill.DayTo
if len(month) == 6 {
month = month[:4] + "-" + month[4:]
}
if len(dayFrom) == 8 {
dayFrom = dayFrom[:4] + "-" + dayFrom[4:6] + "-" + dayFrom[6:]
}
if len(dayTo) == 8 {
dayTo = dayTo[:4] + "-" + dayTo[4:6] + "-" + dayTo[6:]
}
billMaps = append(billMaps, maps.Map{
"id": bill.Id,
"code": bill.Code,
"isPaid": bill.IsPaid,
"month": month,
"dayFrom": dayFrom,
"dayTo": dayTo,
"amount": numberutils.FormatFloat(bill.Amount, 2),
"typeName": bill.TypeName,
"user": userMap,
"description": bill.Description,
"canPay": bill.CanPay,
"pricePeriodName": userconfigs.PricePeriodName(bill.PricePeriod),
"pricePeriod": bill.PricePeriod,
"isOverdue": bill.IsOverdue,
})
}
this.Data["bills"] = billMaps
this.Show()
}

View File

@@ -0,0 +1,20 @@
package bills
import (
"github.com/TeaOSLab/EdgeUser/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth("")).
Data("teaMenu", "finance").
Data("teaSubMenu", "bills").
Prefix("/finance/bills").
Get("", new(IndexAction)).
Post("/pay", new(PayAction)).
Get("/bill", new(BillAction)).
EndAll()
})
}

View File

@@ -0,0 +1,58 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package bills
import (
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/types"
)
type PayAction struct {
actionutils.ParentAction
}
func (this *PayAction) RunPost(params struct {
BillId int64
}) {
defer this.CreateLogInfo(codes.UserBill_LogPayUserBill, params.BillId)
billResp, err := this.RPC().UserBillRPC().FindUserBill(this.UserContext(), &pb.FindUserBillRequest{UserBillId: params.BillId})
if err != nil {
this.ErrorPage(err)
return
}
var bill = billResp.UserBill
if bill == nil {
this.Fail("找不到要支付的账单")
}
if bill.IsPaid {
this.Fail("此账单已支付,不需要重复支付")
}
// 账号余额
accountResp, err := this.RPC().UserAccountRPC().FindEnabledUserAccountWithUserId(this.UserContext(), &pb.FindEnabledUserAccountWithUserIdRequest{UserId: this.UserId()})
if err != nil {
this.ErrorPage(err)
return
}
var account = accountResp.UserAccount
if account == nil {
this.Fail("当前用户还没有账号")
}
if account.Total < bill.Amount {
this.Fail("账号余额不足,当前余额:" + types.String(account.Total) + ",需要支付:" + types.String(bill.Amount))
}
// 支付
_, err = this.RPC().UserBillRPC().PayUserBill(this.UserContext(), &pb.PayUserBillRequest{UserBillId: params.BillId})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,127 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package charge
import (
"encoding/json"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/systemconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
"net/url"
"regexp"
"strings"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "", "")
}
func (this *IndexAction) RunGet(params struct{}) {
this.Data["currentURL"] = url.QueryEscape(this.Request.URL.String())
// 配置
configResp, err := this.RPC().SysSettingRPC().ReadSysSetting(this.UserContext(), &pb.ReadSysSettingRequest{Code: systemconfigs.SettingCodeUserOrderConfig})
if err != nil {
this.ErrorPage(err)
return
}
var config = userconfigs.DefaultUserOrderConfig()
if len(configResp.ValueJSON) > 0 {
err = json.Unmarshal(configResp.ValueJSON, config)
if err != nil {
this.ErrorPage(err)
return
}
}
this.Data["config"] = config
// methods
methodsResp, err := this.RPC().OrderMethodRPC().FindAllAvailableOrderMethods(this.UserContext(), &pb.FindAllAvailableOrderMethodsRequest{})
if err != nil {
this.ErrorPage(err)
return
}
var methods = methodsResp.OrderMethods
var methodMaps = []maps.Map{}
if len(methods) == 0 {
config.EnablePay = false
} else {
for _, method := range methods {
methodMaps = append(methodMaps, maps.Map{
"id": method.Id,
"name": method.Name,
"code": method.Code,
"description": method.Description,
})
}
}
this.Data["methods"] = methodMaps
this.Show()
}
func (this *IndexAction) RunPost(params struct {
Amount string
MethodCode string
Must *actions.Must
CSRF *actionutils.CSRF
}) {
// 检查充值金额
if len(params.Amount) == 0 {
this.FailField("amount", "请输入充值金额")
}
if len(params.Amount) > 10 {
this.FailField("amount", "充值金额不能大于10位")
}
var digitReg = regexp.MustCompile(`^\d+$`)
var decimalReg = regexp.MustCompile(`^\d+\.\d+$`)
if !digitReg.MatchString(params.Amount) && !decimalReg.MatchString(params.Amount) {
this.FailField("amount", "充值金额需要是一个数字")
}
if decimalReg.MatchString(params.Amount) {
var dotIndex = strings.LastIndex(params.Amount, ".")
var decimalString = params.Amount[dotIndex+1:]
if len(decimalString) > 2 {
this.FailField("amount", "充值金额最多只支持两位小数")
}
}
var amount = types.Float64(params.Amount)
if amount <= 0 {
this.FailField("amount", "充值金额不能为0")
}
// 支付方式
if len(params.MethodCode) == 0 {
this.Fail("请选择支付方式")
}
// 生成订单
createResp, err := this.RPC().UserOrderRPC().CreateUserOrder(this.UserContext(), &pb.CreateUserOrderRequest{
Type: userconfigs.OrderTypeCharge,
OrderMethodCode: params.MethodCode,
Amount: amount,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["code"] = createResp.Code
this.Data["payURL"] = createResp.PayURL
this.Success()
}

View File

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

View File

@@ -0,0 +1,26 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package financeutils
import (
"context"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/rpc"
)
// FindUserBalance 读取用户余额
func FindUserBalance(ctx context.Context) (total float64, err error) {
rpcClient, err := rpc.SharedRPC()
if err != nil {
return 0, err
}
accountResp, err := rpcClient.UserAccountRPC().FindEnabledUserAccountWithUserId(ctx, &pb.FindEnabledUserAccountWithUserIdRequest{})
if err != nil {
return 0, err
}
if accountResp.UserAccount != nil {
return accountResp.UserAccount.Total, nil
}
return 0, nil
}

View File

@@ -0,0 +1,64 @@
package bills
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
timeutil "github.com/iwind/TeaGo/utils/time"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "", "")
}
func (this *IndexAction) RunGet(params struct {
Keyword string
}) {
this.Data["keyword"] = params.Keyword
accountResp, err := this.RPC().UserAccountRPC().FindEnabledUserAccountWithUserId(this.UserContext(), &pb.FindEnabledUserAccountWithUserIdRequest{
UserId: this.UserId(),
})
if err != nil {
this.ErrorPage(err)
return
}
var account = accountResp.UserAccount
var accountMap = maps.Map{}
accountMap["total"] = account.Total
accountMap["totalFrozen"] = account.TotalFrozen
this.Data["account"] = accountMap
// 最近操作记录
logsResp, err := this.RPC().UserAccountLogRPC().ListUserAccountLogs(this.UserContext(), &pb.ListUserAccountLogsRequest{
UserAccountId: account.Id,
Offset: 0,
Size: 10,
})
if err != nil {
this.ErrorPage(err)
return
}
var logMaps = []maps.Map{}
for _, log := range logsResp.UserAccountLogs {
logMaps = append(logMaps, maps.Map{
"id": log.Id,
"description": log.Description,
"createdTime": timeutil.FormatTime("Y-m-d H:i:s", log.CreatedAt),
"delta": log.Delta,
"deltaFrozen": log.DeltaFrozen,
"total": log.Total,
"totalFrozen": log.TotalFrozen,
"event": userconfigs.FindAccountEvent(log.EventType),
})
}
this.Data["logs"] = logMaps
this.Show()
}

View File

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

View File

@@ -0,0 +1,84 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package logs
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
"github.com/TeaOSLab/EdgeUser/internal/utils/numberutils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
timeutil "github.com/iwind/TeaGo/utils/time"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "", "")
}
func (this *IndexAction) RunGet(params struct {
Keyword string
EventType string
}) {
this.Data["keyword"] = params.Keyword
this.Data["eventType"] = params.EventType
// 所有事件类型
this.Data["events"] = userconfigs.FindAllAccountEventTypes()
// 数量
countResp, err := this.RPC().UserAccountLogRPC().CountUserAccountLogs(this.UserContext(), &pb.CountUserAccountLogsRequest{
Keyword: params.Keyword,
EventType: params.EventType,
})
if err != nil {
this.ErrorPage(err)
return
}
var count = countResp.Count
var page = this.NewPage(count)
this.Data["page"] = page.AsHTML()
logsResp, err := this.RPC().UserAccountLogRPC().ListUserAccountLogs(this.UserContext(), &pb.ListUserAccountLogsRequest{
Keyword: params.Keyword,
EventType: params.EventType,
Offset: page.Offset,
Size: page.Size,
})
if err != nil {
this.ErrorPage(err)
return
}
var logMaps = []maps.Map{}
for _, log := range logsResp.UserAccountLogs {
var userMap = maps.Map{}
if log.User != nil {
userMap = maps.Map{
"id": log.User.Id,
"username": log.User.Username,
"fullname": log.User.Fullname,
}
}
logMaps = append(logMaps, maps.Map{
"id": log.Id,
"description": log.Description,
"createdTime": timeutil.FormatTime("Y-m-d H:i:s", log.CreatedAt),
"delta": log.Delta,
"deltaFormatted": numberutils.FormatFloat(log.Delta, 2),
"deltaFrozen": log.DeltaFrozen,
"total": log.Total,
"totalFormatted": numberutils.FormatFloat(log.Total, 2),
"totalFrozen": log.TotalFrozen,
"event": userconfigs.FindAccountEvent(log.EventType),
"user": userMap,
"accountId": log.UserAccountId,
})
}
this.Data["logs"] = logMaps
this.Show()
}

View File

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

View File

@@ -0,0 +1,68 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package bills
import (
"encoding/json"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/systemconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/finance/financeutils"
"github.com/iwind/TeaGo/maps"
)
type MethodOptionsAction struct {
actionutils.ParentAction
}
func (this *MethodOptionsAction) RunPost(params struct{}) {
// 配置
configResp, err := this.RPC().SysSettingRPC().ReadSysSetting(this.UserContext(), &pb.ReadSysSettingRequest{Code: systemconfigs.SettingCodeUserOrderConfig})
if err != nil {
this.ErrorPage(err)
return
}
var config = userconfigs.DefaultUserOrderConfig()
if len(configResp.ValueJSON) > 0 {
err = json.Unmarshal(configResp.ValueJSON, config)
if err != nil {
this.ErrorPage(err)
return
}
}
this.Data["config"] = config
// methods
methodsResp, err := this.RPC().OrderMethodRPC().FindAllAvailableOrderMethods(this.UserContext(), &pb.FindAllAvailableOrderMethodsRequest{})
if err != nil {
this.ErrorPage(err)
return
}
var methods = methodsResp.OrderMethods
// 没有可用的支付方式时也不影响支付
var methodMaps = []maps.Map{}
for _, method := range methods {
methodMaps = append(methodMaps, maps.Map{
"id": method.Id,
"name": method.Name,
"code": method.Code,
"description": method.Description,
})
}
this.Data["enablePay"] = config.EnablePay
this.Data["methods"] = methodMaps
// 用户余额
balance, err := financeutils.FindUserBalance(this.UserContext())
if err != nil {
this.ErrorPage(err)
return
}
this.Data["balance"] = balance
this.Success()
}

View File

@@ -0,0 +1,94 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package pay
import (
"errors"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
"github.com/TeaOSLab/EdgeUser/internal/ttlcache"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/rands"
stringutil "github.com/iwind/TeaGo/utils/string"
"github.com/skip2/go-qrcode"
"time"
)
var qrcodeKeySalt = rands.HexString(32)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "", "")
}
func (this *IndexAction) RunGet(params struct {
Code string
From string
ReturnURL string
}) {
this.Data["fromURL"] = params.From
this.Data["returnURL"] = params.ReturnURL
orderResp, err := this.RPC().UserOrderRPC().FindEnabledUserOrder(this.UserContext(), &pb.FindEnabledUserOrderRequest{Code: params.Code})
if err != nil {
this.ErrorPage(err)
return
}
var order = orderResp.UserOrder
if order == nil {
this.ErrorPage(errors.New("can not find order with code '" + params.Code + "'"))
return
}
var methodMap = maps.Map{
"name": "",
}
var qrcodeKey = ""
var qrcodeTitle = ""
if order.OrderMethod != nil {
methodMap = maps.Map{
"name": order.OrderMethod.Name,
}
qrcodeTitle = order.OrderMethod.QrcodeTitle
if len(qrcodeTitle) == 0 && len(order.OrderMethod.ParentCode) > 0 {
var parentDef = userconfigs.FindPresetPayMethodWithCode(order.OrderMethod.ParentCode)
if parentDef != nil {
qrcodeTitle = parentDef.QRCodeTitle
}
}
if order.OrderMethod.ClientType == userconfigs.PayClientTypeMobile {
data, err := qrcode.Encode(order.PayURL, qrcode.Medium, 256)
if err != nil {
this.ErrorPage(errors.New("二维码生成失败:" + err.Error()))
return
}
qrcodeKey = stringutil.Md5(qrcodeKeySalt + ":order:" + order.Code)
ttlcache.DefaultCache.Write(qrcodeKey, data, time.Now().Unix()+600 /** 只保留10分钟防止内存占用过高 **/)
}
}
this.Data["order"] = maps.Map{
"code": order.Code,
"amount": order.Amount,
"canPay": order.CanPay,
"payURL": order.PayURL,
"typeName": userconfigs.FindOrderTypeName(order.Type),
"method": methodMap,
"isExpired": order.IsExpired,
"status": order.Status,
"statusName": userconfigs.FindOrderStatusName(order.Status),
"urlQRCodeKey": qrcodeKey,
"qrcodeTitle": qrcodeTitle,
}
this.Show()
}

View File

@@ -0,0 +1,18 @@
package pay
import (
"github.com/TeaOSLab/EdgeUser/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth("")).
Data("teaMenu", "finance").
Data("teaSubMenu", "orders").
Prefix("/finance/pay").
Get("", new(IndexAction)).
EndAll()
})
}

View File

@@ -0,0 +1,30 @@
package index
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/actions"
)
// CheckOTPAction 检查是否需要OTP
type CheckOTPAction struct {
actionutils.ParentAction
}
func (this *CheckOTPAction) Init() {
this.Nav("", "", "")
}
func (this *CheckOTPAction) RunPost(params struct {
Username string
Must *actions.Must
}) {
checkResp, err := this.RPC().UserRPC().CheckUserOTPWithUsername(this.UserContext(), &pb.CheckUserOTPWithUsernameRequest{Username: params.Username})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["requireOTP"] = checkResp.RequireOTP
this.Success()
}

View File

@@ -0,0 +1,90 @@
package index
import (
"fmt"
"github.com/TeaOSLab/EdgeUser/internal/configloaders"
teaconst "github.com/TeaOSLab/EdgeUser/internal/const"
"github.com/TeaOSLab/EdgeUser/internal/utils/portalutils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/login/loginutils"
"github.com/TeaOSLab/EdgeUser/internal/web/helpers"
stringutil "github.com/iwind/TeaGo/utils/string"
"time"
)
type IndexAction struct {
actionutils.LoginAction
}
// 首页(登录页)
// 修改此页面时需要同步修改 login/index.go
func (this *IndexAction) RunGet(params struct {
From string
Auth *helpers.UserShouldAuth
}) {
// 跳转到自定义页面
if portalutils.HasPortalIndex() {
portalutils.ReadPortalIndex(this.ResponseWriter)
return
}
// 加载UI配置
config, err := configloaders.LoadUIConfig()
if err != nil {
this.ErrorPage(err)
return
}
// 检测是否自动跳转到HTTPS
this.CheckHTTPSRedirecting()
// 跳转到Portal
if config.Portal.IsOn {
this.RedirectURL("/portal")
return
}
// 已登录跳转到dashboard
if params.Auth.IsUser() {
this.RedirectURL("/dashboard")
return
}
this.Data["isUser"] = false
this.Data["menu"] = "signIn"
var timestamp = fmt.Sprintf("%d", time.Now().Unix())
this.Data["token"] = stringutil.Md5(actionutils.TokenSalt+timestamp) + timestamp
this.Data["from"] = params.From
this.Data["systemName"] = config.UserSystemName
this.Data["showVersion"] = config.ShowVersion
if len(config.Version) > 0 {
this.Data["version"] = config.Version
} else {
this.Data["version"] = teaconst.Version
}
this.Data["faviconFileId"] = config.FaviconFileId
this.Data["themeBackgroundColor"] = config.Theme.BackgroundColor
// 是否可以注册
this.Data["canRegister"] = false
this.Data["emailCanLogin"] = false
this.Data["canResetPassword"] = false
{
registerConfig, _ := configloaders.LoadRegisterConfig()
if registerConfig != nil {
this.Data["canRegister"] = registerConfig.IsOn
this.Data["emailCanLogin"] = registerConfig.EmailVerification.IsOn && registerConfig.EmailVerification.CanLogin
this.Data["canResetPassword"] = registerConfig.EmailVerification.IsOn && registerConfig.EmailResetPassword.IsOn
this.Data["mobileCanLogin"] = registerConfig.MobileVerification.IsOn && registerConfig.MobileVerification.CanLogin
}
}
// 删除Cookie
loginutils.UnsetCookie(this.Object())
this.Show()
}

View File

@@ -0,0 +1,14 @@
package index
import (
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.Prefix("").
GetPost("/", new(IndexAction)).
Post("/checkOTP", new(CheckOTPAction)).
EndAll()
})
}

View File

@@ -0,0 +1,547 @@
package lb
import (
"encoding/json"
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/lists"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
"net"
"strconv"
)
type CreateAction struct {
actionutils.ParentAction
}
func (this *CreateAction) Init() {
this.Nav("", "", "create")
}
func (this *CreateAction) RunGet(params struct{}) {
var supportTCP = this.ValidateFeature(userconfigs.UserFeatureCodeServerTCP, 0)
var supportUDP = this.ValidateFeature(userconfigs.UserFeatureCodeServerUDP, 0)
if !supportTCP && !supportUDP {
return
}
this.Data["supportTCP"] = supportTCP
this.Data["supportUDP"] = supportUDP
// 服务类型
var serverTypes = []maps.Map{}
if supportTCP {
serverTypes = append(serverTypes, maps.Map{
"name": "TCP负载均衡",
"code": serverconfigs.ServerTypeTCPProxy,
})
}
if supportUDP {
serverTypes = append(serverTypes, maps.Map{
"name": "UDP负载均衡",
"code": serverconfigs.ServerTypeUDPProxy,
})
}
this.Data["serverTypes"] = serverTypes
this.Data["canSpecifyTCPPort"] = this.ValidateFeature(userconfigs.UserFeatureCodeServerTCPPort, 0)
this.Data["canSpecifyUDPPort"] = this.ValidateFeature(userconfigs.UserFeatureCodeServerUDPPort, 0)
this.Show()
}
func (this *CreateAction) RunPost(params struct {
Name string
ServerType string
Protocols []string
CertIdsJSON []byte
OriginsJSON []byte
TcpPorts []int
TlsPorts []int
UdpPorts []int
Must *actions.Must
CSRF *actionutils.CSRF
}) {
// 检查ServerType
var serverType = params.ServerType
if !lists.ContainsString([]string{serverconfigs.ServerTypeTCPProxy, serverconfigs.ServerTypeUDPProxy}, serverType) {
this.Fail("请选择正确的服务类型")
}
// 检查用户所在集群
clusterIdResp, err := this.RPC().UserRPC().FindUserNodeClusterId(this.UserContext(), &pb.FindUserNodeClusterIdRequest{UserId: this.UserId()})
if err != nil {
this.ErrorPage(err)
return
}
clusterId := clusterIdResp.NodeClusterId
if clusterId == 0 {
this.Fail("当前用户没有指定集群,不能使用此服务")
}
// 检查是否有TCP权限
if lists.ContainsString(params.Protocols, "tcp") && !this.ValidateFeature(userconfigs.UserFeatureCodeServerTCP, 0) {
this.Fail("你没有权限使用TCP负载均衡功能")
}
// 检查是否有UDP权限
if lists.ContainsString(params.Protocols, "udp") && !this.ValidateFeature(userconfigs.UserFeatureCodeServerUDP, 0) {
this.Fail("你没有权限使用UDP负载均衡功能")
}
params.Must.
Field("name", params.Name).
Require("请输入服务名称")
// 协议
if len(params.Protocols) == 0 {
this.Fail("请选择至少一个协议")
}
// TCP
canSpecifyTCPPort := this.ValidateFeature(userconfigs.UserFeatureCodeServerTCPPort, 0)
if serverType == serverconfigs.ServerTypeTCPProxy {
// 检查TCP端口
if canSpecifyTCPPort {
if lists.Contains(params.Protocols, "tcp") {
if len(params.TcpPorts) == 0 {
this.Fail("需要至少指定一个TCP监听端口")
}
for _, port := range params.TcpPorts {
if port < 1024 || port > 65534 {
this.Fail("端口 '" + strconv.Itoa(port) + "' 范围错误")
}
// 检查是否被使用
resp, err := this.RPC().NodeClusterRPC().CheckPortIsUsingInNodeCluster(this.UserContext(), &pb.CheckPortIsUsingInNodeClusterRequest{
Port: types.Int32(port),
NodeClusterId: clusterId,
ProtocolFamily: "tcp",
})
if err != nil {
this.ErrorPage(err)
return
}
if resp.IsUsing {
this.Fail("端口 '" + strconv.Itoa(port) + "' 正在被别的服务使用,请换一个")
}
}
}
if lists.Contains(params.Protocols, "tls") {
if len(params.TlsPorts) == 0 {
this.Fail("需要至少指定一个TLS监听端口")
}
for _, port := range params.TlsPorts {
if port < 1024 || port > 65534 {
this.Fail("端口 '" + strconv.Itoa(port) + "' 范围错误")
}
if lists.ContainsInt(params.TcpPorts, port) {
this.Fail("TLS端口 '" + strconv.Itoa(port) + "' 已经被TCP端口使用不能重复使用")
}
// 检查是否被使用
resp, err := this.RPC().NodeClusterRPC().CheckPortIsUsingInNodeCluster(this.UserContext(), &pb.CheckPortIsUsingInNodeClusterRequest{
Port: types.Int32(port),
NodeClusterId: clusterId,
ProtocolFamily: "tcp",
})
if err != nil {
this.ErrorPage(err)
return
}
if resp.IsUsing {
this.Fail("端口 '" + strconv.Itoa(port) + "' 正在被别的服务使用,请换一个")
}
}
}
}
}
// UDP
canSpecifyUDPPort := this.ValidateFeature(userconfigs.UserFeatureCodeServerUDPPort, 0)
if serverType == serverconfigs.ServerTypeUDPProxy {
// 检查UDP端口
if canSpecifyUDPPort {
if lists.Contains(params.Protocols, "udp") {
if len(params.UdpPorts) == 0 {
this.Fail("需要至少指定一个UDP监听端口")
}
for _, port := range params.UdpPorts {
if port < 1024 || port > 65534 {
this.Fail("端口 '" + strconv.Itoa(port) + "' 范围错误")
}
// 检查是否被使用
resp, err := this.RPC().NodeClusterRPC().CheckPortIsUsingInNodeCluster(this.UserContext(), &pb.CheckPortIsUsingInNodeClusterRequest{
Port: types.Int32(port),
NodeClusterId: clusterId,
ProtocolFamily: "udp",
})
if err != nil {
this.ErrorPage(err)
return
}
if resp.IsUsing {
this.Fail("端口 '" + strconv.Itoa(port) + "' 正在被别的服务使用,请换一个")
}
}
}
}
}
// 先加锁
lockerKey := "create_tcp_server"
lockResp, err := this.RPC().SysLockerRPC().SysLockerLock(this.UserContext(), &pb.SysLockerLockRequest{
Key: lockerKey,
TimeoutSeconds: 30,
})
if err != nil {
this.ErrorPage(err)
return
}
if !lockResp.Ok {
this.Fail("操作繁忙,请稍后再试")
}
defer func() {
_, err := this.RPC().SysLockerRPC().SysLockerUnlock(this.UserContext(), &pb.SysLockerUnlockRequest{Key: lockerKey})
if err != nil {
this.ErrorPage(err)
return
}
}()
tcpConfig := &serverconfigs.TCPProtocolConfig{}
tlsConfig := &serverconfigs.TLSProtocolConfig{}
udpConfig := &serverconfigs.UDPProtocolConfig{}
if serverType == serverconfigs.ServerTypeTCPProxy {
// TCP
ports := []int{}
if lists.ContainsString(params.Protocols, "tcp") {
tcpConfig.IsOn = true
if canSpecifyTCPPort {
for _, port := range params.TcpPorts {
tcpConfig.Listen = append(tcpConfig.Listen, &serverconfigs.NetworkAddressConfig{
Protocol: serverconfigs.ProtocolTCP,
Host: "",
PortRange: strconv.Itoa(port),
})
}
} else {
// 获取随机端口
portResp, err := this.RPC().NodeClusterRPC().FindFreePortInNodeCluster(this.UserContext(), &pb.FindFreePortInNodeClusterRequest{
NodeClusterId: clusterId,
ProtocolFamily: "tcp",
})
if err != nil {
this.ErrorPage(err)
return
}
port := int(portResp.Port)
ports = append(ports, port)
tcpConfig.Listen = []*serverconfigs.NetworkAddressConfig{
{
Protocol: serverconfigs.ProtocolTCP,
Host: "",
PortRange: strconv.Itoa(port),
},
}
}
}
// TLS
if lists.ContainsString(params.Protocols, "tls") {
tlsConfig.IsOn = true
if canSpecifyTCPPort {
for _, port := range params.TlsPorts {
tlsConfig.Listen = append(tlsConfig.Listen, &serverconfigs.NetworkAddressConfig{
Protocol: serverconfigs.ProtocolTLS,
Host: "",
PortRange: strconv.Itoa(port),
})
}
} else {
var port int
// 尝试N次
for i := 0; i < 5; i++ {
// 获取随机端口
portResp, err := this.RPC().NodeClusterRPC().FindFreePortInNodeCluster(this.UserContext(), &pb.FindFreePortInNodeClusterRequest{
NodeClusterId: clusterId,
ProtocolFamily: "tcp",
})
if err != nil {
this.ErrorPage(err)
return
}
p := int(portResp.Port)
if !lists.ContainsInt(ports, p) {
port = p
break
}
}
if port == 0 {
this.Fail("无法找到可用的端口,请稍后重试")
}
tlsConfig.Listen = []*serverconfigs.NetworkAddressConfig{
{
Protocol: serverconfigs.ProtocolTLS,
Host: "",
PortRange: strconv.Itoa(port),
},
}
}
if len(params.CertIdsJSON) == 0 {
this.Fail("请选择或者上传TLS证书")
}
certIds := []int64{}
err := json.Unmarshal(params.CertIdsJSON, &certIds)
if err != nil {
this.ErrorPage(err)
return
}
if len(certIds) == 0 {
this.Fail("请选择或者上传TLS证书")
}
certRefs := []*sslconfigs.SSLCertRef{}
for _, certId := range certIds {
certRefs = append(certRefs, &sslconfigs.SSLCertRef{
IsOn: true,
CertId: certId,
})
}
certRefsJSON, err := json.Marshal(certRefs)
if err != nil {
this.ErrorPage(err)
return
}
// 创建策略
sslPolicyIdResp, err := this.RPC().SSLPolicyRPC().CreateSSLPolicy(this.UserContext(), &pb.CreateSSLPolicyRequest{
Http2Enabled: false,
Http3Enabled: false,
MinVersion: "TLS 1.1",
SslCertsJSON: certRefsJSON,
HstsJSON: nil,
ClientAuthType: 0,
ClientCACertsJSON: nil,
CipherSuites: nil,
CipherSuitesIsOn: false,
})
if err != nil {
this.ErrorPage(err)
return
}
tlsConfig.SSLPolicyRef = &sslconfigs.SSLPolicyRef{
IsOn: true,
SSLPolicyId: sslPolicyIdResp.SslPolicyId,
}
}
}
// UDP
if serverType == serverconfigs.ServerTypeUDPProxy {
if lists.ContainsString(params.Protocols, "udp") {
udpConfig.IsOn = true
if canSpecifyUDPPort {
for _, port := range params.UdpPorts {
udpConfig.Listen = append(udpConfig.Listen, &serverconfigs.NetworkAddressConfig{
Protocol: serverconfigs.ProtocolUDP,
Host: "",
PortRange: strconv.Itoa(port),
})
}
} else {
// 获取随机端口
portResp, err := this.RPC().NodeClusterRPC().FindFreePortInNodeCluster(this.UserContext(), &pb.FindFreePortInNodeClusterRequest{
NodeClusterId: clusterId,
ProtocolFamily: "udp",
})
if err != nil {
this.ErrorPage(err)
return
}
port := int(portResp.Port)
udpConfig.Listen = []*serverconfigs.NetworkAddressConfig{
{
Protocol: serverconfigs.ProtocolUDP,
Host: "",
PortRange: strconv.Itoa(port),
},
}
}
}
}
// 源站信息
originMaps := []maps.Map{}
if len(params.OriginsJSON) == 0 {
this.Fail("请输入源站信息")
}
err = json.Unmarshal(params.OriginsJSON, &originMaps)
if err != nil {
this.ErrorPage(err)
return
}
if len(originMaps) == 0 {
this.Fail("请输入源站信息")
}
primaryOriginRefs := []*serverconfigs.OriginRef{}
backupOriginRefs := []*serverconfigs.OriginRef{}
for _, originMap := range originMaps {
host := originMap.GetString("host")
isPrimary := originMap.GetBool("isPrimary")
scheme := originMap.GetString("scheme")
if len(host) == 0 {
this.Fail("源站地址不能为空")
}
addrHost, addrPort, err := net.SplitHostPort(host)
if err != nil {
this.Fail("源站地址'" + host + "'格式错误")
}
if (serverType == serverconfigs.ServerTypeTCPProxy && scheme != "tcp" && scheme != "tls") ||
(serverType == serverconfigs.ServerTypeUDPProxy && scheme != "udp") {
this.Fail("错误的源站协议")
}
originIdResp, err := this.RPC().OriginRPC().CreateOrigin(this.UserContext(), &pb.CreateOriginRequest{
Name: "",
Addr: &pb.NetworkAddress{
Protocol: scheme,
Host: addrHost,
PortRange: addrPort,
},
Description: "",
Weight: 10,
IsOn: true,
})
if err != nil {
this.ErrorPage(err)
return
}
if isPrimary {
primaryOriginRefs = append(primaryOriginRefs, &serverconfigs.OriginRef{
IsOn: true,
OriginId: originIdResp.OriginId,
})
} else {
backupOriginRefs = append(backupOriginRefs, &serverconfigs.OriginRef{
IsOn: true,
OriginId: originIdResp.OriginId,
})
}
}
primaryOriginsJSON, err := json.Marshal(primaryOriginRefs)
if err != nil {
this.ErrorPage(err)
return
}
backupOriginsJSON, err := json.Marshal(backupOriginRefs)
if err != nil {
this.ErrorPage(err)
return
}
scheduling := &serverconfigs.SchedulingConfig{
Code: "random",
Options: nil,
}
schedulingJSON, err := json.Marshal(scheduling)
if err != nil {
this.ErrorPage(err)
return
}
// 反向代理
reverseProxyResp, err := this.RPC().ReverseProxyRPC().CreateReverseProxy(this.UserContext(), &pb.CreateReverseProxyRequest{
SchedulingJSON: schedulingJSON,
PrimaryOriginsJSON: primaryOriginsJSON,
BackupOriginsJSON: backupOriginsJSON,
})
if err != nil {
this.ErrorPage(err)
return
}
reverseProxyId := reverseProxyResp.ReverseProxyId
reverseProxyRef := &serverconfigs.ReverseProxyRef{
IsPrior: false,
IsOn: true,
ReverseProxyId: reverseProxyId,
}
reverseProxyRefJSON, err := json.Marshal(reverseProxyRef)
if err != nil {
this.ErrorPage(err)
return
}
// 开始保存
var tcpJSON []byte
var tlsJSON []byte
var udpJSON []byte
if tcpConfig.IsOn {
tcpJSON, err = tcpConfig.AsJSON()
if err != nil {
this.ErrorPage(err)
return
}
}
if tlsConfig.IsOn {
tlsJSON, err = tlsConfig.AsJSON()
if err != nil {
this.ErrorPage(err)
return
}
}
if udpConfig.IsOn {
udpJSON, err = udpConfig.AsJSON()
if err != nil {
this.ErrorPage(err)
return
}
}
createResp, err := this.RPC().ServerRPC().CreateServer(this.UserContext(), &pb.CreateServerRequest{
UserId: this.UserId(),
AdminId: 0,
Type: serverType,
Name: params.Name,
Description: "",
ServerNamesJSON: []byte("[]"),
HttpJSON: nil,
HttpsJSON: nil,
TcpJSON: tcpJSON,
TlsJSON: tlsJSON,
UdpJSON: udpJSON,
WebId: 0,
ReverseProxyJSON: reverseProxyRefJSON,
ServerGroupIds: nil,
NodeClusterId: clusterId,
IncludeNodesJSON: nil,
ExcludeNodesJSON: nil,
})
if err != nil {
this.ErrorPage(err)
return
}
serverId := createResp.ServerId
defer this.CreateLogInfo(codes.Server_LogCreateServer, serverId)
this.Success()
}

View File

@@ -0,0 +1,25 @@
package lb
import (
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
)
type DeleteAction struct {
actionutils.ParentAction
}
func (this *DeleteAction) RunPost(params struct {
ServerId int64
}) {
defer this.CreateLogInfo(codes.Server_LogDeleteServer, params.ServerId)
_, err := this.RPC().ServerRPC().DeleteServer(this.UserContext(), &pb.DeleteServerRequest{ServerId: params.ServerId})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,213 @@
package lb
import (
"encoding/json"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
"github.com/TeaOSLab/EdgeUser/internal/configloaders"
"github.com/TeaOSLab/EdgeUser/internal/utils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
timeutil "github.com/iwind/TeaGo/utils/time"
"strings"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "", "index")
}
func (this *IndexAction) RunGet(params struct{}) {
// 更新用户可用状态
stateResp, err := this.RPC().UserRPC().RenewUserServersState(this.UserContext(), &pb.RenewUserServersStateRequest{})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["serversIsEnabled"] = stateResp.IsEnabled
// 提醒有逾期未支付的账单
this.Data["countUnpaidBills"] = 0
priceConfig, _ := configloaders.LoadCacheableUserPriceConfig()
if priceConfig != nil && priceConfig.IsOn && priceConfig.UnpaidBillPolicy.IsOn && (priceConfig.UnpaidBillPolicy.MinDailyBillDays > 0 || priceConfig.UnpaidBillPolicy.MinMonthlyBillDays > 0) {
countResp, err := this.RPC().UserBillRPC().CountAllUserBills(this.UserContext(), &pb.CountAllUserBillsRequest{
PaidFlag: 0,
UserId: this.UserId(),
Month: "",
TrafficRelated: true,
MinDailyBillDays: priceConfig.UnpaidBillPolicy.MinDailyBillDays,
MinMonthlyBillDays: priceConfig.UnpaidBillPolicy.MinMonthlyBillDays,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["countUnpaidBills"] = countResp.Count
}
var supportTCP = this.ValidateFeature(userconfigs.UserFeatureCodeServerTCP, 0)
var supportUDP = this.ValidateFeature(userconfigs.UserFeatureCodeServerUDP, 0)
if !supportTCP && !supportUDP {
return
}
this.Data["supportTCP"] = supportTCP
this.Data["supportUDP"] = supportUDP
var protocols = []string{}
if supportTCP {
protocols = append(protocols, "tcp")
}
if supportUDP {
protocols = append(protocols, "udp")
}
countResp, err := this.RPC().ServerRPC().CountAllEnabledServersMatch(this.UserContext(), &pb.CountAllEnabledServersMatchRequest{
ServerGroupId: 0,
Keyword: "",
UserId: this.UserId(),
ProtocolFamily: strings.Join(protocols, ","),
})
if err != nil {
this.ErrorPage(err)
return
}
var count = countResp.Count
var page = this.NewPage(count)
this.Data["page"] = page.AsHTML()
serversResp, err := this.RPC().ServerRPC().ListEnabledServersMatch(this.UserContext(), &pb.ListEnabledServersMatchRequest{
Offset: page.Offset,
Size: page.Size,
ServerGroupId: 0,
Keyword: "",
ProtocolFamily: strings.Join(protocols, ","),
UserId: this.UserId(),
IgnoreSSLCerts: true,
})
if err != nil {
this.ErrorPage(err)
return
}
var serverMaps = []maps.Map{}
for _, server := range serversResp.Servers {
// CNAME
var cname = ""
if server.NodeCluster != nil {
clusterId := server.NodeCluster.Id
if clusterId > 0 {
dnsInfoResp, err := this.RPC().NodeClusterRPC().FindEnabledNodeClusterDNS(this.UserContext(), &pb.FindEnabledNodeClusterDNSRequest{NodeClusterId: clusterId})
if err != nil {
this.ErrorPage(err)
return
}
if dnsInfoResp.Domain != nil {
cname = server.DnsName + "." + dnsInfoResp.Domain.Name + "."
}
}
}
// TCP
var tcpPorts = []string{}
if len(server.TcpJSON) > 0 {
config, err := serverconfigs.NewTCPProtocolConfigFromJSON(server.TcpJSON)
if err != nil {
this.ErrorPage(err)
return
}
if config.IsOn {
for _, listen := range config.Listen {
tcpPorts = append(tcpPorts, listen.PortRange)
}
}
}
// TLS
var tlsPorts = []string{}
if len(server.TlsJSON) > 0 {
config, err := serverconfigs.NewTLSProtocolConfigFromJSON(server.TlsJSON)
if err != nil {
this.ErrorPage(err)
return
}
if config.IsOn {
for _, listen := range config.Listen {
tlsPorts = append(tlsPorts, listen.PortRange)
}
}
}
// UDP
var udpPorts = []string{}
if len(server.UdpJSON) > 0 {
config, err := serverconfigs.NewUDPProtocolConfigFromJSON(server.UdpJSON)
if err != nil {
this.ErrorPage(err)
return
}
if config.IsOn {
for _, listen := range config.Listen {
udpPorts = append(udpPorts, listen.PortRange)
}
}
}
// 套餐
var userPlanMap = maps.Map{"id": 0}
if server.UserPlanId > 0 {
userPlanResp, err := this.RPC().UserPlanRPC().FindEnabledUserPlan(this.UserContext(), &pb.FindEnabledUserPlanRequest{
UserPlanId: server.UserPlanId,
})
if err != nil {
if !utils.IsNotFound(err) {
this.ErrorPage(err)
return
}
} else {
var userPlan = userPlanResp.UserPlan
if userPlan != nil && userPlan.Plan != nil {
if len(userPlan.Name) == 0 {
userPlan.Name = userPlan.Plan.Name
}
userPlanMap = maps.Map{
"id": userPlan.Id,
"name": userPlan.Name,
"dayTo": userPlan.DayTo,
"isExpired": userPlan.DayTo <= timeutil.Format("Y-m-d"),
}
}
}
}
// 域名和限流状态
var trafficLimitStatus *serverconfigs.TrafficLimitStatus
if len(server.Config) > 0 {
var serverConfig = &serverconfigs.ServerConfig{}
err = json.Unmarshal(server.Config, serverConfig)
if err == nil {
if serverConfig.TrafficLimitStatus != nil && serverConfig.TrafficLimitStatus.IsValid() {
trafficLimitStatus = serverConfig.TrafficLimitStatus
}
}
}
serverMaps = append(serverMaps, maps.Map{
"id": server.Id,
"name": server.Name,
"cname": cname,
"tcpPorts": tcpPorts,
"tlsPorts": tlsPorts,
"udpPorts": udpPorts,
"isOn": server.IsOn,
"userPlan": userPlanMap,
"trafficLimitStatus": trafficLimitStatus,
})
}
this.Data["servers"] = serverMaps
this.Show()
}

View File

@@ -0,0 +1,21 @@
package lb
import (
"github.com/TeaOSLab/EdgeUser/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth("")).
Data("teaMenu", "lb").
Prefix("/lb").
GetPost("", new(IndexAction)).
GetPost("/create", new(CreateAction)).
Post("/updateOn", new(UpdateOnAction)).
Post("/delete", new(DeleteAction)).
Get("/server", new(ServerAction)).
EndAll()
})
}

View File

@@ -0,0 +1,21 @@
package lb
import (
"github.com/TeaOSLab/EdgeUser/internal/utils/numberutils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
)
type ServerAction struct {
actionutils.ParentAction
}
func (this *ServerAction) Init() {
this.Nav("", "", "")
}
func (this *ServerAction) RunGet(params struct {
ServerId int64
}) {
// TODO 先跳转到设置页面,将来实现日志查看、统计看板等
this.RedirectURL("/lb/server/settings/basic?serverId=" + numberutils.FormatInt64(params.ServerId))
}

View File

@@ -0,0 +1,58 @@
package basic
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/actions"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "setting", "index")
this.SecondMenu("basic")
}
func (this *IndexAction) RunGet(params struct {
ServerId int64
}) {
resp, err := this.RPC().ServerRPC().FindEnabledUserServerBasic(this.UserContext(), &pb.FindEnabledUserServerBasicRequest{ServerId: params.ServerId})
if err != nil {
this.ErrorPage(err)
return
}
server := resp.Server
if server == nil {
this.NotFound("server", params.ServerId)
return
}
this.Data["name"] = server.Name
this.Show()
}
func (this *IndexAction) RunPost(params struct {
ServerId int64
Name string
Must *actions.Must
CSRF *actionutils.CSRF
}) {
params.Must.
Field("name", params.Name).
Require("请输入服务名称")
_, err := this.RPC().ServerRPC().UpdateEnabledUserServerBasic(this.UserContext(), &pb.UpdateEnabledUserServerBasicRequest{
ServerId: params.ServerId,
Name: params.Name,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,18 @@
package basic
import (
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/lb/serverutils"
"github.com/TeaOSLab/EdgeUser/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth("")).
Helper(serverutils.NewServerHelper()).
Prefix("/lb/server/settings/basic").
GetPost("", new(IndexAction)).
EndAll()
})
}

View File

@@ -0,0 +1,33 @@
package dns
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "setting", "index")
this.SecondMenu("dns")
}
func (this *IndexAction) RunGet(params struct {
ServerId int64
}) {
dnsInfoResp, err := this.RPC().ServerRPC().FindEnabledServerDNS(this.UserContext(), &pb.FindEnabledServerDNSRequest{ServerId: params.ServerId})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["dnsName"] = dnsInfoResp.DnsName
if dnsInfoResp.Domain != nil {
this.Data["dnsDomain"] = dnsInfoResp.Domain.Name
} else {
this.Data["dnsDomain"] = ""
}
this.Show()
}

View File

@@ -0,0 +1,18 @@
package dns
import (
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/lb/serverutils"
"github.com/TeaOSLab/EdgeUser/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth("")).
Helper(serverutils.NewServerHelper()).
Prefix("/lb/server/settings/dns").
GetPost("", new(IndexAction)).
EndAll()
})
}

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