Initial commit (code only without large binaries)

This commit is contained in:
robin
2026-02-15 18:58:44 +08:00
commit 35df75498f
9442 changed files with 1495866 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
}