Files
waf-platform/EdgeUser/internal/web/helpers/user_must_auth.go
2026-02-14 17:12:38 +08:00

527 lines
14 KiB
Go

package helpers
import (
"encoding/json"
"errors"
"net/http"
"net/url"
"reflect"
"strings"
"github.com/TeaOSLab/EdgeCommon/pkg/configs"
"github.com/TeaOSLab/EdgeCommon/pkg/langs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
"github.com/TeaOSLab/EdgeUser/internal/configloaders"
teaconst "github.com/TeaOSLab/EdgeUser/internal/const"
"github.com/TeaOSLab/EdgeUser/internal/remotelogs"
"github.com/TeaOSLab/EdgeUser/internal/rpc"
"github.com/TeaOSLab/EdgeUser/internal/utils/portalutils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/login/loginutils"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/lists"
"github.com/iwind/TeaGo/maps"
)
// 认证拦截
type userMustAuth struct {
UserId int64
module string
}
func NewUserMustAuth(module string) *userMustAuth {
return &userMustAuth{module: module}
}
func (this *userMustAuth) BeforeAction(actionPtr actions.ActionWrapper, paramName string) (goNext bool) {
var action = actionPtr.Object()
// 检测注入
if !safeFilterRequest(action.Request) {
action.ResponseWriter.WriteHeader(http.StatusForbidden)
_, _ = action.ResponseWriter.Write([]byte("Denied By WAF"))
return false
}
// 安全相关
action.AddHeader("X-Frame-Options", "SAMEORIGIN")
action.AddHeader("Content-Security-Policy", "default-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data:;")
var session = action.Session()
var userId = session.GetInt64(teaconst.SessionUserId)
if userId <= 0 {
var errString = session.GetString("@error")
if len(errString) > 0 {
// 这里为了安全起见不打印具体错误信息
action.WriteString("read session failed, please contact the system administrator.")
return false
}
this.login(action)
return false
}
// 检查指纹
// 将来用户可以自行设置
/**var clientFingerprint = session.GetString("@fingerprint")
if len(clientFingerprint) > 0 && clientFingerprint != loginutils.CalculateClientFingerprint(action) {
loginutils.UnsetCookie(action)
session.Delete()
this.login(action)
return false
}**/
registerConfig, err := configloaders.LoadRegisterConfig()
if err != nil {
remotelogs.Error("LOAD_REGISTER_CONFIG", err.Error())
return false
}
if registerConfig == nil {
action.WriteString("invalid 'registerConfig'")
return false
}
if registerConfig.CheckClientRegion {
var oldClientIP = session.GetString("@ip")
var currentClientIP = loginutils.RemoteIP(action)
if len(oldClientIP) > 0 && len(currentClientIP) > 0 && oldClientIP != currentClientIP {
var oldRegion = loginutils.LookupIPRegion(oldClientIP)
var newRegion = loginutils.LookupIPRegion(currentClientIP)
if newRegion != oldRegion {
loginutils.UnsetCookie(action)
session.Delete()
this.login(action)
return false
}
}
}
this.UserId = userId
action.Context.Set("userId", this.UserId)
userIsOn, fullname, isVerified, isIndividualIdentified, isEnterpriseIdentified, mobileIsVerified, userLang, err := this.findUserInfo(userId)
if err != nil {
if rpc.IsConnError(err) {
remotelogs.Debug("USER_INFO", err.Error())
} else {
remotelogs.Error("USER_INFO", err.Error())
}
action.RedirectURL("/logout")
return false
}
if !userIsOn {
action.ResponseWriter.WriteHeader(http.StatusForbidden)
action.WriteString("You account has been disabled by administrator, please contact with your administrator.")
return false
}
// 强制绑定手机号
if !mobileIsVerified && registerConfig.MobileVerification.Force {
var skipPackages = []string{"settings", "docs", "logout", "files", "csrf", "api", "email", "ui", "tickets", "index", "messages"}
var skip bool
for _, packageName := range skipPackages {
if strings.Contains(action.Spec.PkgPath, "/actions/default/"+packageName) {
skip = true
break
}
}
if !skip {
action.RedirectURL("/settings/mobile-verify")
return false
}
}
var isIdentified = isIndividualIdentified || isEnterpriseIdentified
action.Context.Set("isIndividualIdentified", isIndividualIdentified)
action.Context.Set("isEnterpriseIdentified", isEnterpriseIdentified)
action.Context.Set("isIdentified", isIdentified)
action.Context.Set("isVerified", isVerified)
if len(userLang) == 0 {
userLang = langs.ParseLangFromAction(action)
}
action.Data["teaLang"] = userLang
// 以下是Get专有
if action.Request.Method != http.MethodGet {
return true
}
config, err := configloaders.LoadUIConfig()
if err != nil {
action.WriteString(err.Error())
return false
}
// 初始化内置方法
action.ViewFunc("teaTitle", func() string {
return action.Data["teaTitle"].(string)
})
// 注册 jsonEncode 函数
action.ViewFunc("jsonEncode", func(v interface{}) string {
if v == nil {
return "null"
}
data, err := json.Marshal(v)
if err != nil {
return ""
}
return string(data)
})
action.Data["teaShowVersion"] = config.ShowVersion
action.Data["teaTitle"] = config.UserSystemName
action.Data["teaName"] = config.ProductName
action.Data["teaFaviconFileId"] = config.FaviconFileId
action.Data["teaLogoFileId"] = config.LogoFileId
action.Data["teaUsername"] = fullname
action.Data["teaUserAvatar"] = ""
if !action.Data.Has("teaMenu") {
action.Data["teaMenu"] = ""
}
action.Data["teaModules"] = this.modules(userId, isVerified, isIdentified)
action.Data["teaSubMenus"] = []map[string]interface{}{}
action.Data["teaTabbar"] = []map[string]interface{}{}
// 注入品牌配置
brandConfig := configs.GetBrandConfig()
action.Data["brandConfig"] = brandConfig.ToMap()
if len(config.Version) == 0 {
action.Data["teaVersion"] = teaconst.Version
} else {
action.Data["teaVersion"] = config.Version
}
action.Data["teaShowPageFooter"] = config.ShowPageFooter
action.Data["teaPageFooterHTML"] = config.PageFooterHTML
action.Data["teaThemeBackgroundColor"] = config.Theme.BackgroundColor
action.Data["teaShowIndexPage"] = portalutils.HasPortalIndex()
action.Data["teaIsSuper"] = false
action.Data["teaDemoEnabled"] = false
if !action.Data.Has("teaSubMenu") {
action.Data["teaSubMenu"] = ""
}
// 菜单
action.Data["firstMenuItem"] = ""
// 未读消息数
action.Data["teaBadge"] = 0
// 调用Init
initMethod := reflect.ValueOf(actionPtr).MethodByName("Init")
if initMethod.IsValid() {
initMethod.Call([]reflect.Value{})
}
return true
}
// 菜单配置
func (this *userMustAuth) modules(userId int64, isVerified bool, isIdentified bool) []maps.Map {
// 开通的功能
var featureCodes = []string{}
rpcClient, err := rpc.SharedRPC()
if err == nil {
userFeatureResp, err := rpcClient.UserRPC().FindUserFeatures(rpcClient.Context(userId), &pb.FindUserFeaturesRequest{UserId: userId})
if err == nil {
for _, feature := range userFeatureResp.Features {
featureCodes = append(featureCodes, feature.Code)
}
}
}
registerConfig, err := configloaders.LoadRegisterConfig()
if err != nil {
remotelogs.Error("LOAD_REGISTER_CONFIG", err.Error())
return []maps.Map{}
}
var requireVerification = false
var requireIdentity = false
if registerConfig != nil {
requireVerification = registerConfig.RequireVerification
requireIdentity = registerConfig.RequireIdentity
}
if (requireVerification && !isVerified) || (requireIdentity && !isIdentified) {
featureCodes = []string{}
}
config, _ := configloaders.LoadUIConfig()
var allMaps = []maps.Map{
{
"code": "dashboard",
"name": "概览",
"icon": "dashboard",
"isOn": true,
},
}
var dashboardSubItems = []maps.Map{}
priceConfig, _ := configloaders.LoadCacheableUserPriceConfig()
if (!requireVerification || isVerified) && (!requireIdentity || isIdentified) {
allMaps = []maps.Map{
{
"code": "dashboard",
"name": "概览",
"icon": "dashboard",
"isOn": true,
"subItems": dashboardSubItems,
},
{
"code": "servers",
"name": "CDN加速",
"icon": "clone outline",
"isOn": registerConfig != nil && registerConfig.CDNIsOn,
"subItems": []maps.Map{
{
"name": "刷新预热",
"code": "cache",
"url": "/servers/cache",
},
{
"name": "证书管理",
"code": "certs",
"url": "/servers/certs",
},
{
"name": "证书申请",
"code": "acme",
"url": "/servers/certs/acme",
"isOn": lists.ContainsString(featureCodes, userconfigs.UserFeatureCodeServerACME),
},
{
"name": "用量统计",
"url": "/servers/traffic-stats",
"code": "trafficStat",
"isOn": config != nil && config.ShowBandwidthCharts,
},
{
"name": "计费方式",
"code": "fee",
"url": "/servers/fee",
"isOn": priceConfig != nil && priceConfig.IsOn && !priceConfig.EnablePlans && priceConfig.UserUI.ShowPrices,
},
{
"name": "流量包",
"code": "trafficPackage",
"url": "/servers/packages",
"isOn": priceConfig != nil && priceConfig.IsOn && !priceConfig.EnablePlans && priceConfig.EnableTrafficPackages && priceConfig.ShowTrafficPackages,
},
},
},
{
"code": "dns",
"name": "域名解析",
"icon": "globe",
"isOn": false,
//"subItems": []maps.Map{
// {
// "name": "域名管理",
// "code": "dns",
// "url": "/dns/providers",
// },
//},
},
{
"code": "lb",
"name": "负载均衡",
"icon": "paper plane",
"isOn": registerConfig != nil && registerConfig.CDNIsOn && (lists.ContainsString(featureCodes, "server.tcp") || lists.ContainsString(featureCodes, "server.udp")),
},
{
"code": "waf",
"name": "WAF安全",
"icon": "magnet",
"isOn": registerConfig != nil && registerConfig.CDNIsOn && lists.ContainsString(featureCodes, "server.waf"),
"subItems": []maps.Map{
{
"name": "拦截日志",
"code": "wafLogs",
"url": "/waf/logs",
},
{
"name": "拦截IP",
"code": "iplist",
"url": "/waf/iplists",
},
},
},
{
"code": "plans",
"name": "套餐管理",
"icon": "puzzle piece",
"isOn": registerConfig != nil &&
registerConfig.CDNIsOn &&
lists.ContainsString(featureCodes, "plan") &&
priceConfig != nil &&
priceConfig.IsOn &&
priceConfig.EnablePlans &&
priceConfig.ShowPlansInUserSystem,
},
{
"code": "anti-ddos",
"name": "DDoS高防",
"icon": "shield",
"isOn": registerConfig != nil && registerConfig.ADIsOn,
"subItems": []maps.Map{
{
"name": "实例列表",
"code": "instance",
"url": "/anti-ddos/instances",
},
{
"name": "购买实例",
"code": "package",
"url": "/anti-ddos/packages",
},
},
},
{
"code": "ns",
"name": "智能DNS",
"icon": "globe",
"isOn": registerConfig != nil && registerConfig.NSIsOn,
"subItems": []maps.Map{
{
"name": "我的域名",
"code": "domain",
"url": "/ns/domains",
},
{
"name": "域名分组",
"code": "domainGroup",
"url": "/ns/domains/groups",
},
{
"name": "批量操作",
"code": "domainBatch",
"url": "/ns/domains/batch",
},
{
"name": "线路管理",
"code": "route",
"url": "/ns/routes",
},
{
"name": "套餐",
"code": "plan",
"url": "/ns/plans",
},
/**{
"name": "访问日志",
"code": "accessLog",
"url": "/ns/clusters/accessLogs",
},**/
},
},
{
"code": "finance",
"name": "财务管理",
"icon": "yen sign",
"isOn": true,
"subItems": []maps.Map{
{
"name": "费用账单",
"code": "bills",
"url": "/finance/bills",
},
{
"name": "收支明细",
"code": "logs",
"url": "/finance/logs",
},
{
"name": "充值",
"code": "charge",
"url": "/finance/charge",
},
},
},
{
"code": "tickets",
"name": "工单系统",
"icon": "ticket",
"isOn": true,
},
{
"code": "acl",
"name": "访问控制",
"icon": "address book",
"isOn": true,
},
/**{
"code": "tickets",
"name": "工单",
"icon": "question circle outline",
},**/
}
}
var result = []maps.Map{}
for _, m := range allMaps {
// 财务管理
if m.GetString("code") == "finance" {
if config != nil && !config.ShowFinance {
continue
}
}
// 域名解析
if !m.GetBool("isOn") {
continue
}
result = append(result, m)
}
return result
}
// 跳转到登录页
func (this *userMustAuth) login(action *actions.ActionObject) {
action.RedirectURL("/login?from=" + url.QueryEscape(action.Request.RequestURI))
}
// 查找用户名称
func (this *userMustAuth) findUserInfo(userId int64) (isOn bool, fullname string, isVerified bool, isIndividualIdentified bool, isEnterpriseIdentified bool, mobileIsVerified bool, lang string, err error) {
rpcClient, err := rpc.SharedRPC()
if err != nil {
return false, "", false, false, false, false, "", err
}
resp, err := rpcClient.UserRPC().FindEnabledUser(rpcClient.Context(userId), &pb.FindEnabledUserRequest{UserId: userId})
if err != nil {
return false, "", false, false, false, false, "", err
}
var user = resp.User
if user == nil {
return false, "", false, false, false, false, "", errors.New("can not find user")
}
if !user.IsOn {
return false, "", false, false, false, false, "", nil
}
fullname = user.Fullname
if !user.IsVerified {
fullname += "(未审核)"
} else {
if user.IsRejected {
fullname += "(已被拒绝)"
}
}
return true, fullname, user.IsVerified, user.IsIndividualIdentified, user.IsEnterpriseIdentified, len(user.VerifiedMobile) > 0, user.Lang, nil
}