Initial commit (code only without large binaries)
This commit is contained in:
22
EdgeUser/internal/web/actions/actionutils/csrf.go
Normal file
22
EdgeUser/internal/web/actions/actionutils/csrf.go
Normal 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
|
||||
}
|
||||
135
EdgeUser/internal/web/actions/actionutils/login_action.go
Normal file
135
EdgeUser/internal/web/actions/actionutils/login_action.go
Normal 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()
|
||||
}
|
||||
44
EdgeUser/internal/web/actions/actionutils/menu.go
Normal file
44
EdgeUser/internal/web/actions/actionutils/menu.go
Normal 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
|
||||
}
|
||||
48
EdgeUser/internal/web/actions/actionutils/menu_group.go
Normal file
48
EdgeUser/internal/web/actions/actionutils/menu_group.go
Normal 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
|
||||
}
|
||||
14
EdgeUser/internal/web/actions/actionutils/menu_item.go
Normal file
14
EdgeUser/internal/web/actions/actionutils/menu_item.go
Normal 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"`
|
||||
}
|
||||
161
EdgeUser/internal/web/actions/actionutils/page.go
Normal file
161
EdgeUser/internal/web/actions/actionutils/page.go
Normal 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
|
||||
}
|
||||
40
EdgeUser/internal/web/actions/actionutils/page_test.go
Normal file
40
EdgeUser/internal/web/actions/actionutils/page_test.go
Normal 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)
|
||||
}
|
||||
280
EdgeUser/internal/web/actions/actionutils/parent_action.go
Normal file
280
EdgeUser/internal/web/actions/actionutils/parent_action.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
88
EdgeUser/internal/web/actions/actionutils/portal_action.go
Normal file
88
EdgeUser/internal/web/actions/actionutils/portal_action.go
Normal 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")
|
||||
}
|
||||
42
EdgeUser/internal/web/actions/actionutils/tabbar.go
Normal file
42
EdgeUser/internal/web/actions/actionutils/tabbar.go
Normal 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()
|
||||
}
|
||||
116
EdgeUser/internal/web/actions/actionutils/utils.go
Normal file
116
EdgeUser/internal/web/actions/actionutils/utils.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user