This commit is contained in:
@@ -36,6 +36,9 @@ const TosConfigKey = "system:tos_config"
|
||||
// VerifyCodeConfigKey Verify Code Config Key
|
||||
const VerifyCodeConfigKey = "system:verify_code_config"
|
||||
|
||||
// SignatureConfigKey Signature Config Key
|
||||
const SignatureConfigKey = "system:signature_config"
|
||||
|
||||
// SessionIdKey cache session key
|
||||
const SessionIdKey = "auth:session_id"
|
||||
|
||||
|
||||
+33
-26
@@ -5,36 +5,39 @@ import (
|
||||
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/orm"
|
||||
"github.com/perfect-panel/server/pkg/signature"
|
||||
"github.com/perfect-panel/server/pkg/trace"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Model string `yaml:"Model" default:"prod"`
|
||||
Host string `yaml:"Host" default:"0.0.0.0"`
|
||||
Port int `yaml:"Port" default:"8080"`
|
||||
Debug bool `yaml:"Debug" default:"false"`
|
||||
TLS TLS `yaml:"TLS"`
|
||||
JwtAuth JwtAuth `yaml:"JwtAuth"`
|
||||
Logger logger.LogConf `yaml:"Logger"`
|
||||
MySQL orm.Config `yaml:"MySQL"`
|
||||
Redis RedisConfig `yaml:"Redis"`
|
||||
Site SiteConfig `yaml:"Site"`
|
||||
Node NodeConfig `yaml:"Node"`
|
||||
Mobile MobileConfig `yaml:"Mobile"`
|
||||
Email EmailConfig `yaml:"Email"`
|
||||
Device DeviceConfig `yaml:"device"`
|
||||
Verify Verify `yaml:"Verify"`
|
||||
VerifyCode VerifyCode `yaml:"VerifyCode"`
|
||||
Register RegisterConfig `yaml:"Register"`
|
||||
Subscribe SubscribeConfig `yaml:"Subscribe"`
|
||||
Invite InviteConfig `yaml:"Invite"`
|
||||
Kutt KuttConfig `yaml:"Kutt"`
|
||||
OpenInstall OpenInstallConfig `yaml:"OpenInstall"`
|
||||
Loki LokiConfig `yaml:"Loki"`
|
||||
Telegram Telegram `yaml:"Telegram"`
|
||||
Log Log `yaml:"Log"`
|
||||
Currency Currency `yaml:"Currency"`
|
||||
Trace trace.Config `yaml:"Trace"`
|
||||
Model string `yaml:"Model" default:"prod"`
|
||||
Host string `yaml:"Host" default:"0.0.0.0"`
|
||||
Port int `yaml:"Port" default:"8080"`
|
||||
Debug bool `yaml:"Debug" default:"false"`
|
||||
TLS TLS `yaml:"TLS"`
|
||||
JwtAuth JwtAuth `yaml:"JwtAuth"`
|
||||
Logger logger.LogConf `yaml:"Logger"`
|
||||
MySQL orm.Config `yaml:"MySQL"`
|
||||
Redis RedisConfig `yaml:"Redis"`
|
||||
Site SiteConfig `yaml:"Site"`
|
||||
Node NodeConfig `yaml:"Node"`
|
||||
Mobile MobileConfig `yaml:"Mobile"`
|
||||
Email EmailConfig `yaml:"Email"`
|
||||
Device DeviceConfig `yaml:"device"`
|
||||
AppSignature signature.SignatureConf `yaml:"AppSignature"`
|
||||
Signature Signature `yaml:"Signature"`
|
||||
Verify Verify `yaml:"Verify"`
|
||||
VerifyCode VerifyCode `yaml:"VerifyCode"`
|
||||
Register RegisterConfig `yaml:"Register"`
|
||||
Subscribe SubscribeConfig `yaml:"Subscribe"`
|
||||
Invite InviteConfig `yaml:"Invite"`
|
||||
Kutt KuttConfig `yaml:"Kutt"`
|
||||
OpenInstall OpenInstallConfig `yaml:"OpenInstall"`
|
||||
Loki LokiConfig `yaml:"Loki"`
|
||||
Telegram Telegram `yaml:"Telegram"`
|
||||
Log Log `yaml:"Log"`
|
||||
Currency Currency `yaml:"Currency"`
|
||||
Trace trace.Config `yaml:"Trace"`
|
||||
Administrator struct {
|
||||
Email string `yaml:"Email" default:"admin@ppanel.dev"`
|
||||
Password string `yaml:"Password" default:"password"`
|
||||
@@ -122,6 +125,10 @@ type DeviceConfig struct {
|
||||
SecuritySecret string `yaml:"security_secret"`
|
||||
}
|
||||
|
||||
type Signature struct {
|
||||
EnableSignature bool `yaml:"EnableSignature" default:"false"`
|
||||
}
|
||||
|
||||
type SiteConfig struct {
|
||||
Host string `yaml:"Host" default:""`
|
||||
SiteName string `yaml:"SiteName" default:""`
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/admin/system"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
// Get Signature Config
|
||||
func GetSignatureConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
l := system.NewGetSignatureConfigLogic(c.Request.Context(), svcCtx)
|
||||
resp, err := l.GetSignatureConfig()
|
||||
result.HttpResult(c, resp, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/admin/system"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
// Update Signature Config
|
||||
func UpdateSignatureConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
var req types.SignatureConfig
|
||||
_ = c.ShouldBind(&req)
|
||||
validateErr := svcCtx.Validate(&req)
|
||||
if validateErr != nil {
|
||||
result.ParamErrorResult(c, validateErr)
|
||||
return
|
||||
}
|
||||
|
||||
l := system.NewUpdateSignatureConfigLogic(c.Request.Context(), svcCtx)
|
||||
err := l.UpdateSignatureConfig(&req)
|
||||
result.HttpResult(c, nil, err)
|
||||
}
|
||||
}
|
||||
+24
-79
@@ -28,7 +28,6 @@ import (
|
||||
common "github.com/perfect-panel/server/internal/handler/common"
|
||||
publicAnnouncement "github.com/perfect-panel/server/internal/handler/public/announcement"
|
||||
publicDocument "github.com/perfect-panel/server/internal/handler/public/document"
|
||||
publicIapApple "github.com/perfect-panel/server/internal/handler/public/iap/apple"
|
||||
publicOrder "github.com/perfect-panel/server/internal/handler/public/order"
|
||||
publicPayment "github.com/perfect-panel/server/internal/handler/public/payment"
|
||||
publicPortal "github.com/perfect-panel/server/internal/handler/public/portal"
|
||||
@@ -237,12 +236,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
|
||||
// Filter traffic log details
|
||||
adminLogGroupRouter.GET("/traffic/details", adminLog.FilterTrafficLogDetailsHandler(serverCtx))
|
||||
|
||||
// Error message log list
|
||||
adminLogGroupRouter.GET("/message/error/list", adminLog.GetErrorLogMessageListHandler(serverCtx))
|
||||
|
||||
// Error message log detail
|
||||
adminLogGroupRouter.GET("/message/error/detail", adminLog.GetErrorLogMessageDetailHandler(serverCtx))
|
||||
}
|
||||
|
||||
adminMarketingGroupRouter := router.Group("/v1/admin/marketing")
|
||||
@@ -471,6 +464,12 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
// setting telegram bot
|
||||
adminSystemGroupRouter.POST("/setting_telegram_bot", adminSystem.SettingTelegramBotHandler(serverCtx))
|
||||
|
||||
// Get Signature Config
|
||||
adminSystemGroupRouter.GET("/signature_config", adminSystem.GetSignatureConfigHandler(serverCtx))
|
||||
|
||||
// Update Signature Config
|
||||
adminSystemGroupRouter.PUT("/signature_config", adminSystem.UpdateSignatureConfigHandler(serverCtx))
|
||||
|
||||
// Get site config
|
||||
adminSystemGroupRouter.GET("/site_config", adminSystem.GetSiteConfigHandler(serverCtx))
|
||||
|
||||
@@ -579,6 +578,21 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
// kick offline user device
|
||||
adminUserGroupRouter.PUT("/device/kick_offline", adminUser.KickOfflineByUserDeviceHandler(serverCtx))
|
||||
|
||||
// Get family detail
|
||||
adminUserGroupRouter.GET("/family/detail", adminUser.GetFamilyDetailHandler(serverCtx))
|
||||
|
||||
// Dissolve family
|
||||
adminUserGroupRouter.PUT("/family/dissolve", adminUser.DissolveFamilyHandler(serverCtx))
|
||||
|
||||
// Get family list
|
||||
adminUserGroupRouter.GET("/family/list", adminUser.GetFamilyListHandler(serverCtx))
|
||||
|
||||
// Update family max members
|
||||
adminUserGroupRouter.PUT("/family/max_members", adminUser.UpdateFamilyMaxMembersHandler(serverCtx))
|
||||
|
||||
// Remove family member
|
||||
adminUserGroupRouter.PUT("/family/member/remove", adminUser.RemoveFamilyMemberHandler(serverCtx))
|
||||
|
||||
// Get user list
|
||||
adminUserGroupRouter.GET("/list", adminUser.GetUserListHandler(serverCtx))
|
||||
|
||||
@@ -623,21 +637,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
|
||||
// Get user subcribe traffic logs
|
||||
adminUserGroupRouter.GET("/subscribe/traffic_logs", adminUser.GetUserSubscribeTrafficLogsHandler(serverCtx))
|
||||
|
||||
// Get family list
|
||||
adminUserGroupRouter.GET("/family/list", adminUser.GetFamilyListHandler(serverCtx))
|
||||
|
||||
// Get family detail
|
||||
adminUserGroupRouter.GET("/family/detail", adminUser.GetFamilyDetailHandler(serverCtx))
|
||||
|
||||
// Update family max members
|
||||
adminUserGroupRouter.PUT("/family/max_members", adminUser.UpdateFamilyMaxMembersHandler(serverCtx))
|
||||
|
||||
// Remove family member
|
||||
adminUserGroupRouter.PUT("/family/member/remove", adminUser.RemoveFamilyMemberHandler(serverCtx))
|
||||
|
||||
// Dissolve family
|
||||
adminUserGroupRouter.PUT("/family/dissolve", adminUser.DissolveFamilyHandler(serverCtx))
|
||||
}
|
||||
|
||||
authGroupRouter := router.Group("/v1/auth")
|
||||
@@ -650,18 +649,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
// Check user telephone is exist
|
||||
authGroupRouter.GET("/check/telephone", auth.CheckUserTelephoneHandler(serverCtx))
|
||||
|
||||
// Check legacy verification code
|
||||
authGroupRouter.POST("/check-code", middleware.ApiVersionSwitchHandler(
|
||||
auth.CheckCodeLegacyV1Handler(serverCtx),
|
||||
auth.CheckCodeLegacyV2Handler(serverCtx),
|
||||
))
|
||||
|
||||
// User login
|
||||
authGroupRouter.POST("/login", auth.UserLoginHandler(serverCtx))
|
||||
|
||||
// Email login
|
||||
authGroupRouter.POST("/login/email", auth.EmailLoginHandler(serverCtx))
|
||||
|
||||
// Device Login
|
||||
authGroupRouter.POST("/login/device", auth.DeviceLoginHandler(serverCtx))
|
||||
|
||||
@@ -702,17 +692,11 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
commonGroupRouter.GET("/ads", common.GetAdsHandler(serverCtx))
|
||||
|
||||
// Check verification code
|
||||
commonGroupRouter.POST("/check_verification_code", middleware.ApiVersionSwitchHandler(
|
||||
common.CheckVerificationCodeV1Handler(serverCtx),
|
||||
common.CheckVerificationCodeV2Handler(serverCtx),
|
||||
))
|
||||
commonGroupRouter.POST("/check_verification_code", common.CheckVerificationCodeHandler(serverCtx))
|
||||
|
||||
// Get Client
|
||||
commonGroupRouter.GET("/client", common.GetClientHandler(serverCtx))
|
||||
|
||||
// Get Download Link
|
||||
commonGroupRouter.GET("/client/download", common.GetDownloadLinkHandler(serverCtx))
|
||||
|
||||
// Heartbeat
|
||||
commonGroupRouter.GET("/heartbeat", common.HeartbeatHandler(serverCtx))
|
||||
|
||||
@@ -733,12 +717,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
|
||||
// Get Tos Content
|
||||
commonGroupRouter.GET("/site/tos", common.GetTosHandler(serverCtx))
|
||||
|
||||
// Submit contact info
|
||||
commonGroupRouter.POST("/contact", common.SubmitContactHandler(serverCtx))
|
||||
|
||||
// Report client error log
|
||||
commonGroupRouter.POST("/log/message/report", common.ReportLogMessageHandler(serverCtx))
|
||||
}
|
||||
|
||||
publicAnnouncementGroupRouter := router.Group("/v1/public/announcement")
|
||||
@@ -828,15 +806,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
publicRedemptionGroupRouter.POST("/", publicRedemption.RedeemCodeHandler(serverCtx))
|
||||
}
|
||||
|
||||
iapAppleGroupRouter := router.Group("/v1/public/iap/apple")
|
||||
iapAppleGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
|
||||
{
|
||||
iapAppleGroupRouter.GET("/status", publicIapApple.GetAppleStatusHandler(serverCtx))
|
||||
iapAppleGroupRouter.POST("/transactions/attach", publicIapApple.AttachAppleTransactionHandler(serverCtx))
|
||||
iapAppleGroupRouter.POST("/transactions/attach_by_id", publicIapApple.AttachAppleTransactionByIdHandler(serverCtx))
|
||||
iapAppleGroupRouter.POST("/restore", publicIapApple.RestoreAppleTransactionsHandler(serverCtx))
|
||||
}
|
||||
|
||||
publicSubscribeGroupRouter := router.Group("/v1/public/subscribe")
|
||||
publicSubscribeGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
|
||||
|
||||
@@ -961,30 +930,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
|
||||
// Query Withdrawal Log
|
||||
publicUserGroupRouter.GET("/withdrawal_log", publicUser.QueryWithdrawalLogHandler(serverCtx))
|
||||
|
||||
// Bind Email With Verification
|
||||
publicUserGroupRouter.POST("/bind_email_with_verification", publicUser.BindEmailWithVerificationHandler(serverCtx))
|
||||
|
||||
// Bind Invite Code
|
||||
publicUserGroupRouter.POST("/bind_invite_code", publicUser.BindInviteCodeHandler(serverCtx))
|
||||
|
||||
// Get Subscribe Status
|
||||
publicUserGroupRouter.POST("/subscribe_status", publicUser.GetSubscribeStatusHandler(serverCtx))
|
||||
|
||||
// Delete Account
|
||||
publicUserGroupRouter.POST("/delete_account", publicUser.DeleteAccountHandler(serverCtx))
|
||||
|
||||
// Get agent realtime data
|
||||
publicUserGroupRouter.GET("/agent/realtime", publicUser.GetAgentRealtimeHandler(serverCtx))
|
||||
|
||||
// Get agent downloads data
|
||||
publicUserGroupRouter.GET("/agent/downloads", publicUser.GetAgentDownloadsHandler(serverCtx))
|
||||
|
||||
// Get user invite statistics
|
||||
publicUserGroupRouter.GET("/invite/stats", publicUser.GetUserInviteStatsHandler(serverCtx))
|
||||
|
||||
// Get invite sales data
|
||||
publicUserGroupRouter.GET("/invite/sales", publicUser.GetInviteSalesHandler(serverCtx))
|
||||
}
|
||||
|
||||
publicUserWsGroupRouter := router.Group("/v1/public/user")
|
||||
@@ -1015,10 +960,10 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
serverGroupRouter.GET("/user", server.GetServerUserListHandler(serverCtx))
|
||||
}
|
||||
|
||||
serverV2GroupRouter := router.Group("/v2/server")
|
||||
serverGroupRouter := router.Group("/v2/server")
|
||||
|
||||
{
|
||||
// Get Server Protocol Config
|
||||
serverV2GroupRouter.GET("/:server_id", server.QueryServerProtocolConfigHandler(serverCtx))
|
||||
serverGroupRouter.GET("/:server_id", server.QueryServerProtocolConfigHandler(serverCtx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/tool"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type GetSignatureConfigLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// Get Signature Config
|
||||
func NewGetSignatureConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetSignatureConfigLogic {
|
||||
return &GetSignatureConfigLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *GetSignatureConfigLogic) GetSignatureConfig() (resp *types.SignatureConfig, err error) {
|
||||
resp = &types.SignatureConfig{}
|
||||
configs, err := l.svcCtx.SystemModel.GetSignatureConfig(l.ctx)
|
||||
if err != nil {
|
||||
l.Errorw("[GetSignatureConfig] Database query error", logger.Field("error", err.Error()))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get signature config error: %v", err.Error())
|
||||
}
|
||||
tool.SystemConfigSliceReflectToStruct(configs, resp)
|
||||
return resp, nil
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
|
||||
"github.com/perfect-panel/server/initialize"
|
||||
"github.com/perfect-panel/server/internal/config"
|
||||
"github.com/perfect-panel/server/internal/model/system"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/tool"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UpdateSignatureConfigLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// Update Signature Config
|
||||
func NewUpdateSignatureConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateSignatureConfigLogic {
|
||||
return &UpdateSignatureConfigLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *UpdateSignatureConfigLogic) UpdateSignatureConfig(req *types.SignatureConfig) error {
|
||||
v := reflect.ValueOf(*req)
|
||||
t := v.Type()
|
||||
err := l.svcCtx.SystemModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
fieldName := t.Field(i).Name
|
||||
fieldValue := tool.ConvertValueToString(v.Field(i))
|
||||
if err := db.Model(&system.System{}).Where("`category` = 'signature' and `key` = ?", fieldName).Update("value", fieldValue).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return l.svcCtx.Redis.Del(l.ctx, config.SignatureConfigKey, config.GlobalConfigKey).Err()
|
||||
})
|
||||
if err != nil {
|
||||
l.Errorw("[UpdateSignatureConfig] update signature config error", logger.Field("error", err.Error()))
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update signature config error: %v", err.Error())
|
||||
}
|
||||
initialize.Signature(l.svcCtx)
|
||||
return nil
|
||||
}
|
||||
@@ -47,6 +47,7 @@ func (l *GetGlobalConfigLogic) GetGlobalConfig() (resp *types.GetGlobalConfigRes
|
||||
tool.DeepCopy(&resp.Auth.Email, l.svcCtx.Config.Email)
|
||||
tool.DeepCopy(&resp.Auth.Mobile, l.svcCtx.Config.Mobile)
|
||||
tool.DeepCopy(&resp.Auth.Register, l.svcCtx.Config.Register)
|
||||
tool.DeepCopy(&resp.Signature, l.svcCtx.Config.Signature)
|
||||
tool.DeepCopy(&resp.Verify, l.svcCtx.Config.Verify)
|
||||
tool.DeepCopy(&resp.Invite, l.svcCtx.Config.Invite)
|
||||
tool.SystemConfigSliceReflectToStruct(currencyCfg, &resp.Currency)
|
||||
|
||||
@@ -113,16 +113,17 @@ func (l *QueryUserSubscribeNodeListLogic) getServers(userSub *user.Subscribe) (u
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe details error: %v", err.Error())
|
||||
}
|
||||
nodeIds := tool.StringToInt64Slice(subDetails.Nodes)
|
||||
tags := strings.Split(subDetails.NodeTags, ",")
|
||||
tags := normalizeSubscribeNodeTags(subDetails.NodeTags)
|
||||
|
||||
l.Debugf("[Generate Subscribe]nodes: %v, NodeTags: %v", nodeIds, tags)
|
||||
|
||||
enable := true
|
||||
|
||||
_, nodes, err := l.svcCtx.NodeModel.FilterNodeList(l.ctx, &node.FilterNodeParams{
|
||||
Page: 0,
|
||||
Page: 1,
|
||||
Size: 1000,
|
||||
NodeId: nodeIds,
|
||||
Tag: tags,
|
||||
Enabled: &enable, // Only get enabled nodes
|
||||
})
|
||||
|
||||
@@ -213,3 +214,21 @@ func (l *QueryUserSubscribeNodeListLogic) getUserSubscribe(token string) (*user.
|
||||
|
||||
return userSub, nil
|
||||
}
|
||||
|
||||
func normalizeSubscribeNodeTags(raw string) []string {
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
parts := strings.Split(raw, ",")
|
||||
cleaned := make([]string, 0, len(parts))
|
||||
for _, tag := range parts {
|
||||
trimmed := strings.TrimSpace(tag)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
cleaned = append(cleaned, trimmed)
|
||||
}
|
||||
|
||||
return tool.RemoveDuplicateElements(cleaned...)
|
||||
}
|
||||
|
||||
@@ -23,3 +23,11 @@ func TestFillUserSubscribeInfoEntitlementFields(t *testing.T) {
|
||||
require.Equal(t, int64(3001), sub.EntitlementOwnerUserId)
|
||||
require.True(t, sub.ReadOnly)
|
||||
}
|
||||
|
||||
func TestNormalizeSubscribeNodeTags(t *testing.T) {
|
||||
tags := normalizeSubscribeNodeTags("美国, 日本, , 美国, ,日本")
|
||||
require.Equal(t, []string{"美国", "日本"}, tags)
|
||||
|
||||
empty := normalizeSubscribeNodeTags("")
|
||||
require.Nil(t, empty)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,10 @@ func CorsMiddleware(c *gin.Context) {
|
||||
}
|
||||
// c.Writer.Header().Set("Access-Control-Allow-Origin", c.Request.Host)
|
||||
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Origin, X-CSRF-Token, Authorization, AccessToken, Token, Range, api-header")
|
||||
c.Writer.Header().Set(
|
||||
"Access-Control-Allow-Headers",
|
||||
"Content-Type, Origin, X-CSRF-Token, Authorization, AccessToken, Token, Range, api-header, X-Signature-Enabled, X-App-Id, X-Timestamp, X-Nonce, X-Signature",
|
||||
)
|
||||
c.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
c.Writer.Header().Set("Access-Control-Max-Age", "172800")
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
"github.com/perfect-panel/server/pkg/signature"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
)
|
||||
|
||||
var (
|
||||
publicSignaturePrefixes = []string{
|
||||
"/v1/auth",
|
||||
"/v1/auth/oauth",
|
||||
"/v1/common",
|
||||
"/v1/public",
|
||||
}
|
||||
defaultSignatureSkipPrefixes = []string{
|
||||
"/v1/notify/",
|
||||
"/v1/iap/notifications",
|
||||
"/v1/telegram/webhook",
|
||||
"/v1/subscribe/config",
|
||||
}
|
||||
)
|
||||
|
||||
func SignatureMiddleware(srvCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
skipPrefixes := collectSignatureSkipPrefixes(srvCtx)
|
||||
return func(c *gin.Context) {
|
||||
path := c.Request.URL.Path
|
||||
if !isPublicSignaturePath(path) {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
for _, prefix := range skipPrefixes {
|
||||
if strings.HasPrefix(path, prefix) {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
if !srvCtx.Config.Signature.EnableSignature {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
if c.GetHeader("X-Signature-Enabled") != "1" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
appId := c.GetHeader("X-App-Id")
|
||||
if appId == "" {
|
||||
result.HttpResult(c, nil, xerr.NewErrCode(xerr.InvalidAccess))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
timestamp := c.GetHeader("X-Timestamp")
|
||||
nonce := c.GetHeader("X-Nonce")
|
||||
sig := c.GetHeader("X-Signature")
|
||||
if timestamp == "" || nonce == "" || sig == "" {
|
||||
result.HttpResult(c, nil, xerr.NewErrCode(xerr.SignatureMissing))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
var bodyBytes []byte
|
||||
if c.Request.Body != nil {
|
||||
bodyBytes, _ = io.ReadAll(c.Request.Body)
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
}
|
||||
|
||||
sts := signature.BuildStringToSign(
|
||||
c.Request.Method,
|
||||
path,
|
||||
c.Request.URL.RawQuery,
|
||||
bodyBytes,
|
||||
appId,
|
||||
timestamp,
|
||||
nonce,
|
||||
)
|
||||
if err := srvCtx.SignatureValidator.Validate(c.Request.Context(), appId, timestamp, nonce, sig, sts); err != nil {
|
||||
result.HttpResult(c, nil, xerr.NewErrCode(mapSignatureErr(err)))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func isPublicSignaturePath(path string) bool {
|
||||
for _, prefix := range publicSignaturePrefixes {
|
||||
if strings.HasPrefix(path, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func collectSignatureSkipPrefixes(srvCtx *svc.ServiceContext) []string {
|
||||
prefixSet := make(map[string]struct{}, len(defaultSignatureSkipPrefixes)+len(srvCtx.Config.AppSignature.SkipPrefixes)+1)
|
||||
for _, prefix := range defaultSignatureSkipPrefixes {
|
||||
prefixSet[prefix] = struct{}{}
|
||||
}
|
||||
for _, prefix := range srvCtx.Config.AppSignature.SkipPrefixes {
|
||||
if strings.TrimSpace(prefix) == "" {
|
||||
continue
|
||||
}
|
||||
prefixSet[prefix] = struct{}{}
|
||||
}
|
||||
if path := strings.TrimSpace(srvCtx.Config.Subscribe.SubscribePath); path != "" {
|
||||
prefixSet[path] = struct{}{}
|
||||
}
|
||||
|
||||
prefixes := make([]string, 0, len(prefixSet))
|
||||
for prefix := range prefixSet {
|
||||
prefixes = append(prefixes, prefix)
|
||||
}
|
||||
return prefixes
|
||||
}
|
||||
|
||||
func mapSignatureErr(err error) uint32 {
|
||||
switch err {
|
||||
case signature.ErrSignatureMissing:
|
||||
return xerr.SignatureMissing
|
||||
case signature.ErrSignatureExpired:
|
||||
return xerr.SignatureExpired
|
||||
case signature.ErrSignatureReplay:
|
||||
return xerr.SignatureReplay
|
||||
default:
|
||||
return xerr.SignatureInvalid
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/config"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/pkg/signature"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
)
|
||||
|
||||
type testNonceStore struct {
|
||||
seen map[string]bool
|
||||
}
|
||||
|
||||
func newTestNonceStore() *testNonceStore {
|
||||
return &testNonceStore{seen: map[string]bool{}}
|
||||
}
|
||||
|
||||
func (s *testNonceStore) SetIfNotExists(_ context.Context, appId, nonce string, _ int64) (bool, error) {
|
||||
key := appId + ":" + nonce
|
||||
if s.seen[key] {
|
||||
return true, nil
|
||||
}
|
||||
s.seen[key] = true
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func makeTestSignature(secret, sts string) string {
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
mac.Write([]byte(sts))
|
||||
return hex.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
|
||||
func newTestServiceContext() *svc.ServiceContext {
|
||||
conf := config.Config{}
|
||||
conf.Signature.EnableSignature = true
|
||||
conf.AppSignature = signature.SignatureConf{
|
||||
AppSecrets: map[string]string{
|
||||
"web-client": "test-secret",
|
||||
},
|
||||
ValidWindowSeconds: 300,
|
||||
SkipPrefixes: []string{
|
||||
"/v1/public/health",
|
||||
},
|
||||
}
|
||||
return &svc.ServiceContext{
|
||||
Config: conf,
|
||||
SignatureValidator: signature.NewValidator(conf.AppSignature, newTestNonceStore()),
|
||||
}
|
||||
}
|
||||
|
||||
func newTestServiceContextWithSwitch(enabled bool) *svc.ServiceContext {
|
||||
svcCtx := newTestServiceContext()
|
||||
svcCtx.Config.Signature.EnableSignature = enabled
|
||||
return svcCtx
|
||||
}
|
||||
|
||||
func decodeCode(t *testing.T, body []byte) uint32 {
|
||||
t.Helper()
|
||||
var resp struct {
|
||||
Code uint32 `json:"code"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &resp); err != nil {
|
||||
t.Fatalf("unmarshal response failed: %v", err)
|
||||
}
|
||||
return resp.Code
|
||||
}
|
||||
|
||||
func TestSignatureMiddlewareMissingAppID(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
svcCtx := newTestServiceContext()
|
||||
r := gin.New()
|
||||
r.Use(SignatureMiddleware(svcCtx))
|
||||
r.GET("/v1/public/ping", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/public/ping", nil)
|
||||
req.Header.Set("X-Signature-Enabled", "1")
|
||||
resp := httptest.NewRecorder()
|
||||
r.ServeHTTP(resp, req)
|
||||
|
||||
if code := decodeCode(t, resp.Body.Bytes()); code != xerr.InvalidAccess {
|
||||
t.Fatalf("expected InvalidAccess(%d), got %d", xerr.InvalidAccess, code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignatureMiddlewareMissingSignatureHeaders(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
svcCtx := newTestServiceContext()
|
||||
r := gin.New()
|
||||
r.Use(SignatureMiddleware(svcCtx))
|
||||
r.GET("/v1/public/ping", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/public/ping", nil)
|
||||
req.Header.Set("X-Signature-Enabled", "1")
|
||||
req.Header.Set("X-App-Id", "web-client")
|
||||
resp := httptest.NewRecorder()
|
||||
r.ServeHTTP(resp, req)
|
||||
|
||||
if code := decodeCode(t, resp.Body.Bytes()); code != xerr.SignatureMissing {
|
||||
t.Fatalf("expected SignatureMissing(%d), got %d", xerr.SignatureMissing, code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignatureMiddlewarePassesWhenSignatureHeaderMissing(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
svcCtx := newTestServiceContext()
|
||||
r := gin.New()
|
||||
r.Use(SignatureMiddleware(svcCtx))
|
||||
r.GET("/v1/public/ping", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/public/ping", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
r.ServeHTTP(resp, req)
|
||||
|
||||
if resp.Code != http.StatusOK || resp.Body.String() != "ok" {
|
||||
t.Fatalf("expected pass-through without X-Signature-Enabled, got code=%d body=%s", resp.Code, resp.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignatureMiddlewarePassesWhenSignatureHeaderIsZero(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
svcCtx := newTestServiceContext()
|
||||
r := gin.New()
|
||||
r.Use(SignatureMiddleware(svcCtx))
|
||||
r.GET("/v1/public/ping", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/public/ping", nil)
|
||||
req.Header.Set("X-Signature-Enabled", "0")
|
||||
resp := httptest.NewRecorder()
|
||||
r.ServeHTTP(resp, req)
|
||||
|
||||
if resp.Code != http.StatusOK || resp.Body.String() != "ok" {
|
||||
t.Fatalf("expected pass-through when X-Signature-Enabled=0, got code=%d body=%s", resp.Code, resp.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignatureMiddlewarePassesWhenSystemSwitchDisabled(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
svcCtx := newTestServiceContextWithSwitch(false)
|
||||
r := gin.New()
|
||||
r.Use(SignatureMiddleware(svcCtx))
|
||||
r.GET("/v1/public/ping", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/public/ping", nil)
|
||||
req.Header.Set("X-Signature-Enabled", "1")
|
||||
resp := httptest.NewRecorder()
|
||||
r.ServeHTTP(resp, req)
|
||||
|
||||
if resp.Code != http.StatusOK || resp.Body.String() != "ok" {
|
||||
t.Fatalf("expected pass-through when system switch is disabled, got code=%d body=%s", resp.Code, resp.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignatureMiddlewareSkipsNonPublicPath(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
svcCtx := newTestServiceContext()
|
||||
r := gin.New()
|
||||
r.Use(SignatureMiddleware(svcCtx))
|
||||
r.GET("/v1/admin/ping", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/admin/ping", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
r.ServeHTTP(resp, req)
|
||||
|
||||
if resp.Code != http.StatusOK || resp.Body.String() != "ok" {
|
||||
t.Fatalf("expected pass-through for non-public path, got code=%d body=%s", resp.Code, resp.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignatureMiddlewareHonorsSkipPrefix(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
svcCtx := newTestServiceContext()
|
||||
r := gin.New()
|
||||
r.Use(SignatureMiddleware(svcCtx))
|
||||
r.GET("/v1/public/healthz", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/public/healthz", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
r.ServeHTTP(resp, req)
|
||||
|
||||
if resp.Code != http.StatusOK || resp.Body.String() != "ok" {
|
||||
t.Fatalf("expected skip-prefix pass-through, got code=%d body=%s", resp.Code, resp.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignatureMiddlewareRestoresBodyAfterVerify(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
svcCtx := newTestServiceContext()
|
||||
r := gin.New()
|
||||
r.Use(SignatureMiddleware(svcCtx))
|
||||
r.POST("/v1/public/body", func(c *gin.Context) {
|
||||
body, _ := io.ReadAll(c.Request.Body)
|
||||
c.String(http.StatusOK, string(body))
|
||||
})
|
||||
|
||||
body := `{"hello":"world"}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/public/body?a=1&b=2", strings.NewReader(body))
|
||||
ts := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
nonce := "nonce-body-1"
|
||||
sts := signature.BuildStringToSign(http.MethodPost, "/v1/public/body", "a=1&b=2", []byte(body), "web-client", ts, nonce)
|
||||
req.Header.Set("X-Signature-Enabled", "1")
|
||||
req.Header.Set("X-App-Id", "web-client")
|
||||
req.Header.Set("X-Timestamp", ts)
|
||||
req.Header.Set("X-Nonce", nonce)
|
||||
req.Header.Set("X-Signature", makeTestSignature("test-secret", sts))
|
||||
resp := httptest.NewRecorder()
|
||||
r.ServeHTTP(resp, req)
|
||||
|
||||
if resp.Code != http.StatusOK || resp.Body.String() != body {
|
||||
t.Fatalf("expected restored body, got code=%d body=%s", resp.Code, resp.Body.String())
|
||||
}
|
||||
}
|
||||
@@ -202,27 +202,36 @@ func (m *customServerModel) ClearServerAllCache(ctx context.Context) error {
|
||||
|
||||
// CountNodesByIdsAndTags 根据节点ID和标签计算启用的节点数量
|
||||
func (m *customServerModel) CountNodesByIdsAndTags(ctx context.Context, nodeIds []int64, tags []string) (int64, error) {
|
||||
tags = normalizeNodeTags(tags)
|
||||
if len(nodeIds) == 0 && len(tags) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var count int64
|
||||
query := m.WithContext(ctx).Model(&Node{}).Where("enabled = ?", true)
|
||||
|
||||
if len(nodeIds) > 0 || len(tags) > 0 {
|
||||
subQuery := m.WithContext(ctx).Model(&Node{}).Where("enabled = ?", true)
|
||||
|
||||
if len(nodeIds) > 0 && len(tags) > 0 {
|
||||
subQuery = subQuery.Where("id IN ? OR ?", nodeIds, InSet("tags", tags))
|
||||
} else if len(nodeIds) > 0 {
|
||||
subQuery = subQuery.Where("id IN ?", nodeIds)
|
||||
} else {
|
||||
subQuery = subQuery.Scopes(InSet("tags", tags))
|
||||
}
|
||||
|
||||
query = subQuery
|
||||
if len(nodeIds) > 0 {
|
||||
query = query.Where("id IN ?", nodeIds)
|
||||
}
|
||||
if len(tags) > 0 {
|
||||
query = query.Scopes(InSet("tags", tags))
|
||||
}
|
||||
|
||||
err := query.Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func normalizeNodeTags(tags []string) []string {
|
||||
cleaned := make([]string, 0, len(tags))
|
||||
for _, tag := range tags {
|
||||
trimmed := strings.TrimSpace(tag)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
cleaned = append(cleaned, trimmed)
|
||||
}
|
||||
return tool.RemoveDuplicateElements(cleaned...)
|
||||
}
|
||||
|
||||
// InSet 支持多值 OR 查询
|
||||
func InSet(field string, values []string) func(db *gorm.DB) *gorm.DB {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNormalizeNodeTags(t *testing.T) {
|
||||
tags := normalizeNodeTags([]string{"美国", " 日本 ", "", "美国", " ", "日本"})
|
||||
require.Equal(t, []string{"美国", "日本"}, tags)
|
||||
}
|
||||
@@ -15,7 +15,8 @@ import (
|
||||
|
||||
var _ Model = (*customSubscribeModel)(nil)
|
||||
var (
|
||||
cacheSubscribeIdPrefix = "cache:subscribe:id:"
|
||||
cacheSubscribeIdPrefix = "cache:subscribe:id:"
|
||||
cacheUserSubscribeUserPrefix = "cache:user:subscribe:user:"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -119,13 +120,31 @@ func (m *defaultSubscribeModel) Update(ctx context.Context, data *Subscribe, tx
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
var userIds []int64
|
||||
err = m.QueryNoCacheCtx(ctx, &userIds, func(conn *gorm.DB, v interface{}) error {
|
||||
return conn.Table("user_subscribe").
|
||||
Where("subscribe_id = ? AND status IN (0, 1)", data.Id).
|
||||
Distinct("user_id").
|
||||
Pluck("user_id", &userIds).Error
|
||||
})
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
userSubscribeCacheKeys := make([]string, 0, len(userIds))
|
||||
for _, userId := range userIds {
|
||||
userSubscribeCacheKeys = append(userSubscribeCacheKeys, fmt.Sprintf("%s%d", cacheUserSubscribeUserPrefix, userId))
|
||||
}
|
||||
allCacheKeys := append(m.getCacheKeys(old), userSubscribeCacheKeys...)
|
||||
|
||||
err = m.ExecCtx(ctx, func(conn *gorm.DB) error {
|
||||
db := conn
|
||||
if len(tx) > 0 {
|
||||
db = tx[0]
|
||||
}
|
||||
return db.Save(data).Error
|
||||
}, m.getCacheKeys(old)...)
|
||||
}, allCacheKeys...)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -137,13 +156,31 @@ func (m *defaultSubscribeModel) Delete(ctx context.Context, id int64, tx ...*gor
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var userIds []int64
|
||||
err = m.QueryNoCacheCtx(ctx, &userIds, func(conn *gorm.DB, v interface{}) error {
|
||||
return conn.Table("user_subscribe").
|
||||
Where("subscribe_id = ? AND status IN (0, 1)", id).
|
||||
Distinct("user_id").
|
||||
Pluck("user_id", &userIds).Error
|
||||
})
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
userSubscribeCacheKeys := make([]string, 0, len(userIds))
|
||||
for _, userId := range userIds {
|
||||
userSubscribeCacheKeys = append(userSubscribeCacheKeys, fmt.Sprintf("%s%d", cacheUserSubscribeUserPrefix, userId))
|
||||
}
|
||||
allCacheKeys := append(m.getCacheKeys(data), userSubscribeCacheKeys...)
|
||||
|
||||
err = m.ExecCtx(ctx, func(conn *gorm.DB) error {
|
||||
db := conn
|
||||
if len(tx) > 0 {
|
||||
db = tx[0]
|
||||
}
|
||||
return db.Delete(&Subscribe{}, id).Error
|
||||
}, m.getCacheKeys(data)...)
|
||||
}, allCacheKeys...)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ type customSystemLogicModel interface {
|
||||
GetTosConfig(ctx context.Context) ([]*System, error)
|
||||
GetCurrencyConfig(ctx context.Context) ([]*System, error)
|
||||
GetVerifyCodeConfig(ctx context.Context) ([]*System, error)
|
||||
GetSignatureConfig(ctx context.Context) ([]*System, error)
|
||||
GetLogConfig(ctx context.Context) ([]*System, error)
|
||||
UpdateNodeMultiplierConfig(ctx context.Context, config string) error
|
||||
FindNodeMultiplierConfig(ctx context.Context) (*System, error)
|
||||
@@ -154,6 +155,15 @@ func (m *customSystemModel) GetVerifyCodeConfig(ctx context.Context) ([]*System,
|
||||
return configs, err
|
||||
}
|
||||
|
||||
// GetSignatureConfig returns the signature config.
|
||||
func (m *customSystemModel) GetSignatureConfig(ctx context.Context) ([]*System, error) {
|
||||
var configs []*System
|
||||
err := m.QueryCtx(ctx, &configs, config.SignatureConfigKey, func(conn *gorm.DB, v interface{}) error {
|
||||
return conn.Where("`category` = ?", "signature").Find(v).Error
|
||||
})
|
||||
return configs, err
|
||||
}
|
||||
|
||||
// GetLogConfig returns the log config.
|
||||
func (m *customSystemModel) GetLogConfig(ctx context.Context) ([]*System, error) {
|
||||
var configs []*System
|
||||
|
||||
+8
-1
@@ -50,7 +50,14 @@ func initServer(svc *svc.ServiceContext) *gin.Engine {
|
||||
}
|
||||
r.Use(sessions.Sessions("ppanel", sessionStore))
|
||||
// use cors middleware
|
||||
r.Use(middleware.TraceMiddleware(svc), middleware.ApiVersionMiddleware(svc), middleware.LoggerMiddleware(svc), middleware.CorsMiddleware, gin.Recovery())
|
||||
r.Use(
|
||||
middleware.TraceMiddleware(svc),
|
||||
middleware.ApiVersionMiddleware(svc),
|
||||
middleware.LoggerMiddleware(svc),
|
||||
middleware.SignatureMiddleware(svc),
|
||||
middleware.CorsMiddleware,
|
||||
gin.Recovery(),
|
||||
)
|
||||
|
||||
// register handlers
|
||||
handler.RegisterHandlers(r, svc)
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/perfect-panel/server/pkg/limit"
|
||||
"github.com/perfect-panel/server/pkg/nodeMultiplier"
|
||||
"github.com/perfect-panel/server/pkg/orm"
|
||||
"github.com/perfect-panel/server/pkg/signature"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
"github.com/hibiken/asynq"
|
||||
@@ -36,33 +37,34 @@ import (
|
||||
)
|
||||
|
||||
type ServiceContext struct {
|
||||
DB *gorm.DB
|
||||
Redis *redis.Client
|
||||
Config config.Config
|
||||
Queue *asynq.Client
|
||||
ExchangeRate float64
|
||||
GeoIP *IPLocation
|
||||
DB *gorm.DB
|
||||
Redis *redis.Client
|
||||
Config config.Config
|
||||
Queue *asynq.Client
|
||||
ExchangeRate float64
|
||||
GeoIP *IPLocation
|
||||
SignatureValidator *signature.Validator
|
||||
|
||||
//NodeCache *cache.NodeCacheClient
|
||||
AuthModel auth.Model
|
||||
AdsModel ads.Model
|
||||
LogModel log.Model
|
||||
AuthModel auth.Model
|
||||
AdsModel ads.Model
|
||||
LogModel log.Model
|
||||
LogMessageModel logmessage.Model
|
||||
NodeModel node.Model
|
||||
UserModel user.Model
|
||||
OrderModel order.Model
|
||||
ClientModel client.Model
|
||||
TicketModel ticket.Model
|
||||
NodeModel node.Model
|
||||
UserModel user.Model
|
||||
OrderModel order.Model
|
||||
ClientModel client.Model
|
||||
TicketModel ticket.Model
|
||||
//ServerModel server.Model
|
||||
SystemModel system.Model
|
||||
CouponModel coupon.Model
|
||||
RedemptionCodeModel redemption.RedemptionCodeModel
|
||||
RedemptionRecordModel redemption.RedemptionRecordModel
|
||||
PaymentModel payment.Model
|
||||
DocumentModel document.Model
|
||||
SubscribeModel subscribe.Model
|
||||
TrafficLogModel traffic.Model
|
||||
AnnouncementModel announcement.Model
|
||||
SystemModel system.Model
|
||||
CouponModel coupon.Model
|
||||
RedemptionCodeModel redemption.RedemptionCodeModel
|
||||
RedemptionRecordModel redemption.RedemptionRecordModel
|
||||
PaymentModel payment.Model
|
||||
DocumentModel document.Model
|
||||
SubscribeModel subscribe.Model
|
||||
TrafficLogModel traffic.Model
|
||||
AnnouncementModel announcement.Model
|
||||
IAPAppleTransactionModel iapapple.Model
|
||||
|
||||
Restart func() error
|
||||
@@ -109,24 +111,27 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
||||
_ = rds.FlushAll(context.Background()).Err()
|
||||
}
|
||||
authLimiter := limit.NewPeriodLimit(86400, 15, rds, config.SendCountLimitKeyPrefix, limit.Align())
|
||||
nonceStore := signature.NewRedisNonceStore(rds)
|
||||
|
||||
srv := &ServiceContext{
|
||||
DB: db,
|
||||
Redis: rds,
|
||||
Config: c,
|
||||
Queue: NewAsynqClient(c),
|
||||
ExchangeRate: 0,
|
||||
GeoIP: geoIP,
|
||||
DB: db,
|
||||
Redis: rds,
|
||||
Config: c,
|
||||
Queue: NewAsynqClient(c),
|
||||
ExchangeRate: 0,
|
||||
GeoIP: geoIP,
|
||||
SignatureValidator: signature.NewValidator(c.AppSignature, nonceStore),
|
||||
//NodeCache: cache.NewNodeCacheClient(rds),
|
||||
AuthLimiter: authLimiter,
|
||||
AdsModel: ads.NewModel(db, rds),
|
||||
LogModel: log.NewModel(db),
|
||||
AuthLimiter: authLimiter,
|
||||
AdsModel: ads.NewModel(db, rds),
|
||||
LogModel: log.NewModel(db),
|
||||
LogMessageModel: logmessage.NewModel(db),
|
||||
NodeModel: node.NewModel(db, rds),
|
||||
AuthModel: auth.NewModel(db, rds),
|
||||
UserModel: user.NewModel(db, rds),
|
||||
OrderModel: order.NewModel(db, rds),
|
||||
ClientModel: client.NewSubscribeApplicationModel(db),
|
||||
TicketModel: ticket.NewModel(db, rds),
|
||||
NodeModel: node.NewModel(db, rds),
|
||||
AuthModel: auth.NewModel(db, rds),
|
||||
UserModel: user.NewModel(db, rds),
|
||||
OrderModel: order.NewModel(db, rds),
|
||||
ClientModel: client.NewSubscribeApplicationModel(db),
|
||||
TicketModel: ticket.NewModel(db, rds),
|
||||
//ServerModel: server.NewModel(db, rds),
|
||||
SystemModel: system.NewModel(db, rds),
|
||||
CouponModel: coupon.NewModel(db, rds),
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package types
|
||||
|
||||
type ContactRequest struct {
|
||||
Name string `json:"name" validate:"required,max=100"`
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
OtherContact string `json:"other_contact" validate:"max=200"`
|
||||
Notes string `json:"notes" validate:"max=2000"`
|
||||
}
|
||||
|
||||
type GetDownloadLinkRequest struct {
|
||||
InviteCode string `form:"invite_code,optional"`
|
||||
Platform string `form:"platform" validate:"required,oneof=windows mac ios android"`
|
||||
}
|
||||
|
||||
type GetDownloadLinkResponse struct {
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
type ReportLogMessageRequest struct {
|
||||
Platform string `json:"platform" validate:"required,max=32"`
|
||||
AppVersion string `json:"app_version" validate:"required,max=64"`
|
||||
OsName string `json:"os_name" validate:"max=64"`
|
||||
OsVersion string `json:"os_version" validate:"max=64"`
|
||||
DeviceId string `json:"device_id" validate:"required,max=255"`
|
||||
UserId int64 `json:"user_id"`
|
||||
SessionId string `json:"session_id" validate:"max=255"`
|
||||
Level uint8 `json:"level"`
|
||||
ErrorCode string `json:"error_code" validate:"max=128"`
|
||||
Message string `json:"message" validate:"required,max=65535"`
|
||||
Stack string `json:"stack" validate:"max=1048576"`
|
||||
Context interface{} `json:"context"`
|
||||
OccurredAt int64 `json:"occurred_at"`
|
||||
}
|
||||
|
||||
type ReportLogMessageResponse struct {
|
||||
Id int64 `json:"id"`
|
||||
}
|
||||
|
||||
type LegacyCheckVerificationCodeRequest struct {
|
||||
Method string `json:"method" form:"method" validate:"omitempty,oneof=email mobile"`
|
||||
Account string `json:"account" form:"account"`
|
||||
Email string `json:"email" form:"email"`
|
||||
Code string `json:"code" form:"code" validate:"required"`
|
||||
Type uint8 `json:"type" form:"type" validate:"required"`
|
||||
}
|
||||
|
||||
type LegacyCheckVerificationCodeResponse struct {
|
||||
Status bool `json:"status"`
|
||||
Exist bool `json:"exist"`
|
||||
}
|
||||
|
||||
type EmailLoginRequest struct {
|
||||
Identifier string `json:"identifier" form:"identifier"`
|
||||
Email string `json:"email" form:"email" validate:"required,email"`
|
||||
Code string `json:"code" form:"code" validate:"required"`
|
||||
Invite string `json:"invite" form:"invite"`
|
||||
IP string `header:"X-Original-Forwarded-For"`
|
||||
UserAgent string `header:"User-Agent"`
|
||||
LoginType string `header:"Login-Type"`
|
||||
CfToken string `json:"cf_token,optional" form:"cf_token"`
|
||||
}
|
||||
+70
-302
@@ -221,19 +221,6 @@ type CheckVerificationCodeRespone struct {
|
||||
Exist bool `json:"exist,omitempty"`
|
||||
}
|
||||
|
||||
type LegacyCheckVerificationCodeRequest struct {
|
||||
Method string `json:"method" form:"method"`
|
||||
Account string `json:"account" form:"account"`
|
||||
Email string `json:"email" form:"email"`
|
||||
Code string `json:"code" form:"code" validate:"required"`
|
||||
Type uint8 `json:"type" form:"type" validate:"required"`
|
||||
}
|
||||
|
||||
type LegacyCheckVerificationCodeResponse struct {
|
||||
Status bool `json:"status"`
|
||||
Exist bool `json:"exist"`
|
||||
}
|
||||
|
||||
type CheckoutOrderRequest struct {
|
||||
OrderNo string `json:"orderNo"`
|
||||
ReturnUrl string `json:"returnUrl,omitempty"`
|
||||
@@ -576,6 +563,11 @@ type DeviceLoginRequest struct {
|
||||
ShortCode string `json:"short_code,optional"`
|
||||
}
|
||||
|
||||
type DissolveFamilyRequest struct {
|
||||
FamilyId int64 `json:"family_id" validate:"required,gt=0"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
type Document struct {
|
||||
Id int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
@@ -615,6 +607,34 @@ type EmailAuthticateConfig struct {
|
||||
DomainSuffixList string `json:"domain_suffix_list"`
|
||||
}
|
||||
|
||||
type FamilyDetail struct {
|
||||
Summary FamilySummary `json:"summary"`
|
||||
Members []FamilyMemberItem `json:"members"`
|
||||
}
|
||||
|
||||
type FamilyMemberItem struct {
|
||||
UserId int64 `json:"user_id"`
|
||||
Identifier string `json:"identifier"`
|
||||
Role uint8 `json:"role"`
|
||||
RoleName string `json:"role_name"`
|
||||
Status uint8 `json:"status"`
|
||||
StatusName string `json:"status_name"`
|
||||
JoinSource string `json:"join_source"`
|
||||
JoinedAt int64 `json:"joined_at"`
|
||||
LeftAt int64 `json:"left_at,omitempty"`
|
||||
}
|
||||
|
||||
type FamilySummary struct {
|
||||
FamilyId int64 `json:"family_id"`
|
||||
OwnerUserId int64 `json:"owner_user_id"`
|
||||
OwnerIdentifier string `json:"owner_identifier"`
|
||||
Status string `json:"status"`
|
||||
ActiveMemberCount int64 `json:"active_member_count"`
|
||||
MaxMembers int64 `json:"max_members"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
|
||||
type FilterBalanceLogRequest struct {
|
||||
FilterLogParams
|
||||
UserId int64 `form:"user_id,optional"`
|
||||
@@ -887,6 +907,25 @@ type GetDocumentListResponse struct {
|
||||
List []Document `json:"list"`
|
||||
}
|
||||
|
||||
type GetFamilyDetailRequest struct {
|
||||
Id int64 `form:"id" validate:"required"`
|
||||
}
|
||||
|
||||
type GetFamilyListRequest struct {
|
||||
Page int `form:"page"`
|
||||
Size int `form:"size"`
|
||||
Keyword string `form:"keyword,omitempty"`
|
||||
Status string `form:"status,omitempty"`
|
||||
OwnerUserId *int64 `form:"owner_user_id,omitempty"`
|
||||
FamilyId *int64 `form:"family_id,omitempty"`
|
||||
UserId *int64 `form:"user_id,omitempty"`
|
||||
}
|
||||
|
||||
type GetFamilyListResponse struct {
|
||||
List []FamilySummary `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
type GetGlobalConfigResponse struct {
|
||||
Site SiteConfig `json:"site"`
|
||||
Verify VeifyConfig `json:"verify"`
|
||||
@@ -894,6 +933,7 @@ type GetGlobalConfigResponse struct {
|
||||
Invite InviteConfig `json:"invite"`
|
||||
Currency Currency `json:"currency"`
|
||||
Subscribe SubscribeConfig `json:"subscribe"`
|
||||
Signature SignatureConfig `json:"signature"`
|
||||
VerifyCode PubilcVerifyCodeConfig `json:"verify_code"`
|
||||
OAuthMethods []string `json:"oauth_methods"`
|
||||
WebAd bool `json:"web_ad"`
|
||||
@@ -1188,41 +1228,6 @@ type GetUserSubscribeResetTrafficLogsResponse struct {
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
type GetFamilyListRequest struct {
|
||||
Page int `form:"page"`
|
||||
Size int `form:"size"`
|
||||
Keyword string `form:"keyword,omitempty"`
|
||||
Status string `form:"status,omitempty"`
|
||||
OwnerUserId *int64 `form:"owner_user_id,omitempty"`
|
||||
FamilyId *int64 `form:"family_id,omitempty"`
|
||||
UserId *int64 `form:"user_id,omitempty"`
|
||||
}
|
||||
|
||||
type GetFamilyListResponse struct {
|
||||
List []FamilySummary `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
type GetFamilyDetailRequest struct {
|
||||
Id int64 `form:"id" validate:"required"`
|
||||
}
|
||||
|
||||
type UpdateFamilyMaxMembersRequest struct {
|
||||
FamilyId int64 `json:"family_id" validate:"required,gt=0"`
|
||||
MaxMembers int64 `json:"max_members" validate:"required,gt=0"`
|
||||
}
|
||||
|
||||
type RemoveFamilyMemberRequest struct {
|
||||
FamilyId int64 `json:"family_id" validate:"required,gt=0"`
|
||||
UserId int64 `json:"user_id" validate:"required,gt=0"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
type DissolveFamilyRequest struct {
|
||||
FamilyId int64 `json:"family_id" validate:"required,gt=0"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
type GetUserSubscribeTrafficLogsRequest struct {
|
||||
Page int `form:"page"`
|
||||
Size int `form:"size"`
|
||||
@@ -1706,10 +1711,8 @@ type QueryOrderDetailRequest struct {
|
||||
}
|
||||
|
||||
type QueryOrderListRequest struct {
|
||||
Page int `form:"page" validate:"required"`
|
||||
Size int `form:"size" validate:"required"`
|
||||
Status int `form:"status"`
|
||||
Search string `form:"search"`
|
||||
Page int `form:"page" validate:"required"`
|
||||
Size int `form:"size" validate:"required"`
|
||||
}
|
||||
|
||||
type QueryOrderListResponse struct {
|
||||
@@ -1932,6 +1935,12 @@ type RegisterLog struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
type RemoveFamilyMemberRequest struct {
|
||||
FamilyId int64 `json:"family_id" validate:"required,gt=0"`
|
||||
UserId int64 `json:"user_id" validate:"required,gt=0"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
type RenewalOrderRequest struct {
|
||||
UserSubscribeID int64 `json:"user_subscribe_id"`
|
||||
Quantity int64 `json:"quantity" validate:"lte=1000"`
|
||||
@@ -2163,6 +2172,10 @@ type ShadowsocksProtocol struct {
|
||||
Method string `json:"method"`
|
||||
}
|
||||
|
||||
type SignatureConfig struct {
|
||||
EnableSignature bool `json:"enable_signature"`
|
||||
}
|
||||
|
||||
type SiteConfig struct {
|
||||
Host string `json:"host"`
|
||||
SiteName string `json:"site_name"`
|
||||
@@ -2523,6 +2536,11 @@ type UpdateDocumentRequest struct {
|
||||
Show *bool `json:"show"`
|
||||
}
|
||||
|
||||
type UpdateFamilyMaxMembersRequest struct {
|
||||
FamilyId int64 `json:"family_id" validate:"required,gt=0"`
|
||||
MaxMembers int64 `json:"max_members" validate:"required,gt=0"`
|
||||
}
|
||||
|
||||
type UpdateNodeRequest struct {
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@@ -2699,7 +2717,6 @@ type User struct {
|
||||
GiftAmount int64 `json:"gift_amount"`
|
||||
Telegram int64 `json:"telegram"`
|
||||
ReferCode string `json:"refer_code"`
|
||||
ShareLink string `json:"share_link,omitempty"`
|
||||
RefererId int64 `json:"referer_id"`
|
||||
Enable bool `json:"enable"`
|
||||
IsAdmin bool `json:"is_admin,omitempty"`
|
||||
@@ -2716,8 +2733,6 @@ type User struct {
|
||||
IsDel bool `json:"is_del,omitempty"`
|
||||
Remark string `json:"remark,omitempty"`
|
||||
PurchasedPackage string `json:"purchased_package,omitempty"`
|
||||
LastLoginTime int64 `json:"last_login_time"`
|
||||
MemberStatus string `json:"member_status"`
|
||||
FamilyJoined bool `json:"family_joined,omitempty"`
|
||||
FamilyId int64 `json:"family_id,omitempty"`
|
||||
FamilyRole uint8 `json:"family_role,omitempty"`
|
||||
@@ -2761,34 +2776,6 @@ type UserLoginLog struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
type FamilySummary struct {
|
||||
FamilyId int64 `json:"family_id"`
|
||||
OwnerUserId int64 `json:"owner_user_id"`
|
||||
OwnerIdentifier string `json:"owner_identifier"`
|
||||
Status string `json:"status"`
|
||||
ActiveMemberCount int64 `json:"active_member_count"`
|
||||
MaxMembers int64 `json:"max_members"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
|
||||
type FamilyMemberItem struct {
|
||||
UserId int64 `json:"user_id"`
|
||||
Identifier string `json:"identifier"`
|
||||
Role uint8 `json:"role"`
|
||||
RoleName string `json:"role_name"`
|
||||
Status uint8 `json:"status"`
|
||||
StatusName string `json:"status_name"`
|
||||
JoinSource string `json:"join_source"`
|
||||
JoinedAt int64 `json:"joined_at"`
|
||||
LeftAt int64 `json:"left_at,omitempty"`
|
||||
}
|
||||
|
||||
type FamilyDetail struct {
|
||||
Summary FamilySummary `json:"summary"`
|
||||
Members []FamilyMemberItem `json:"members"`
|
||||
}
|
||||
|
||||
type UserLoginRequest struct {
|
||||
Identifier string `json:"identifier"`
|
||||
Email string `json:"email" validate:"required"`
|
||||
@@ -2844,7 +2831,6 @@ type UserSubscribe struct {
|
||||
EntitlementOwnerUserId int64 `json:"entitlement_owner_user_id"`
|
||||
ReadOnly bool `json:"read_only"`
|
||||
Short string `json:"short"`
|
||||
IsGift bool `json:"is_gift"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
@@ -3023,221 +3009,3 @@ type WithdrawalLog struct {
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
|
||||
type EmailLoginRequest struct {
|
||||
Identifier string `json:"identifier"`
|
||||
Email string `json:"email" validate:"required"`
|
||||
Code string `json:"code" validate:"required"`
|
||||
Invite string `json:"invite,optional"`
|
||||
IP string `header:"X-Original-Forwarded-For"`
|
||||
UserAgent string `header:"User-Agent"`
|
||||
LoginType string `header:"Login-Type"`
|
||||
CfToken string `json:"cf_token,optional"`
|
||||
}
|
||||
|
||||
type BindEmailWithVerificationRequest struct {
|
||||
Email string `json:"email" validate:"required"`
|
||||
Code string `json:"code" validate:"required"`
|
||||
}
|
||||
|
||||
type BindEmailWithVerificationResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Token string `json:"token,omitempty"`
|
||||
UserId int64 `json:"user_id,omitempty"`
|
||||
FamilyJoined bool `json:"family_joined,omitempty"`
|
||||
FamilyId int64 `json:"family_id,omitempty"`
|
||||
OwnerUserId int64 `json:"owner_user_id,omitempty"`
|
||||
}
|
||||
|
||||
type BindInviteCodeRequest struct {
|
||||
InviteCode string `json:"invite_code" validate:"required"`
|
||||
}
|
||||
|
||||
type GetSubscribeStatusResponse struct {
|
||||
DeviceStatus bool `json:"device_status"`
|
||||
EmailStatus bool `json:"email_status"`
|
||||
}
|
||||
|
||||
type GetSubscribeStatusRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
}
|
||||
|
||||
type DeleteAccountResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message,omitempty"`
|
||||
UserId int64 `json:"user_id"`
|
||||
Code int `json:"code"`
|
||||
}
|
||||
|
||||
type GetDownloadLinkRequest struct {
|
||||
InviteCode string `form:"invite_code,optional"`
|
||||
Platform string `form:"platform" validate:"required,oneof=windows mac ios android"`
|
||||
}
|
||||
|
||||
type GetDownloadLinkResponse struct {
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
type ContactRequest struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
OtherContact string `json:"other_contact,omitempty"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
}
|
||||
|
||||
type ReportLogMessageRequest struct {
|
||||
Platform string `json:"platform" validate:"required"`
|
||||
AppVersion string `json:"appVersion"`
|
||||
OsName string `json:"osName"`
|
||||
OsVersion string `json:"osVersion"`
|
||||
DeviceId string `json:"deviceId"`
|
||||
UserId int64 `json:"userId"`
|
||||
SessionId string `json:"sessionId"`
|
||||
Level uint8 `json:"level"`
|
||||
ErrorCode string `json:"errorCode"`
|
||||
Message string `json:"message" validate:"required"`
|
||||
Stack string `json:"stack"`
|
||||
Context map[string]interface{} `json:"context"`
|
||||
OccurredAt int64 `json:"occurredAt"`
|
||||
}
|
||||
|
||||
type ReportLogMessageResponse struct {
|
||||
Id int64 `json:"id"`
|
||||
}
|
||||
|
||||
type GetErrorLogMessageListRequest struct {
|
||||
Page int `form:"page"`
|
||||
Size int `form:"size"`
|
||||
Platform string `form:"platform"`
|
||||
Level uint8 `form:"level"`
|
||||
UserId int64 `form:"user_id"`
|
||||
DeviceId string `form:"device_id"`
|
||||
ErrorCode string `form:"error_code"`
|
||||
Keyword string `form:"keyword"`
|
||||
Start int64 `form:"start"`
|
||||
End int64 `form:"end"`
|
||||
}
|
||||
|
||||
type ErrorLogMessage struct {
|
||||
Id int64 `json:"id"`
|
||||
Platform string `json:"platform"`
|
||||
AppVersion string `json:"app_version"`
|
||||
OsName string `json:"os_name"`
|
||||
OsVersion string `json:"os_version"`
|
||||
DeviceId string `json:"device_id"`
|
||||
UserId int64 `json:"user_id"`
|
||||
SessionId string `json:"session_id"`
|
||||
Level uint8 `json:"level"`
|
||||
ErrorCode string `json:"error_code"`
|
||||
Message string `json:"message"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
|
||||
type GetErrorLogMessageListResponse struct {
|
||||
Total int64 `json:"total"`
|
||||
List []ErrorLogMessage `json:"list"`
|
||||
}
|
||||
|
||||
type GetErrorLogMessageDetailResponse struct {
|
||||
Id int64 `json:"id"`
|
||||
Platform string `json:"platform"`
|
||||
AppVersion string `json:"app_version"`
|
||||
OsName string `json:"os_name"`
|
||||
OsVersion string `json:"os_version"`
|
||||
DeviceId string `json:"device_id"`
|
||||
UserId int64 `json:"user_id"`
|
||||
SessionId string `json:"session_id"`
|
||||
Level uint8 `json:"level"`
|
||||
ErrorCode string `json:"error_code"`
|
||||
Message string `json:"message"`
|
||||
Stack string `json:"stack"`
|
||||
Context map[string]interface{} `json:"context"`
|
||||
ClientIP string `json:"client_ip"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Locale string `json:"locale"`
|
||||
OccurredAt int64 `json:"occurred_at"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
|
||||
type AttachAppleTransactionRequest struct {
|
||||
SignedTransactionJWS string `json:"signed_transaction_jws" validate:"required"`
|
||||
DurationDays int64 `json:"duration_days,omitempty"`
|
||||
Tier string `json:"tier,omitempty"`
|
||||
SubscribeId int64 `json:"subscribe_id,omitempty"`
|
||||
OrderNo string `json:"order_no" validate:"required"`
|
||||
}
|
||||
|
||||
type AttachAppleTransactionResponse struct {
|
||||
ExpiresAt int64 `json:"expires_at"`
|
||||
Tier string `json:"tier"`
|
||||
}
|
||||
|
||||
type AttachAppleTransactionByIdRequest struct {
|
||||
TransactionId string `json:"transaction_id" validate:"required"`
|
||||
OrderNo string `json:"order_no" validate:"required"`
|
||||
Sandbox *bool `json:"sandbox,omitempty"`
|
||||
}
|
||||
|
||||
type RestoreAppleTransactionsRequest struct {
|
||||
Transactions []string `json:"transactions" validate:"required"`
|
||||
}
|
||||
|
||||
type GetAppleStatusResponse struct {
|
||||
Active bool `json:"active"`
|
||||
ExpiresAt int64 `json:"expires_at"`
|
||||
Tier string `json:"tier"`
|
||||
}
|
||||
|
||||
type GetAgentRealtimeRequest struct{}
|
||||
|
||||
type GetAgentRealtimeResponse struct {
|
||||
Total int64 `json:"total"`
|
||||
Clicks int64 `json:"clicks"`
|
||||
Views int64 `json:"views"`
|
||||
Installs int64 `json:"installs"`
|
||||
PaidCount int64 `json:"paid_count"`
|
||||
GrowthRate string `json:"growth_rate"`
|
||||
PaidGrowthRate string `json:"paid_growth_rate"`
|
||||
}
|
||||
|
||||
type GetAgentDownloadsRequest struct{}
|
||||
|
||||
type GetAgentDownloadsResponse struct {
|
||||
Total int64 `json:"total"`
|
||||
Platforms *PlatformDownloads `json:"platforms"`
|
||||
ComparisonRate *string `json:"comparison_rate,omitempty"`
|
||||
}
|
||||
|
||||
type PlatformDownloads struct {
|
||||
IOS int64 `json:"ios"`
|
||||
Android int64 `json:"android"`
|
||||
Windows int64 `json:"windows"`
|
||||
Mac int64 `json:"mac"`
|
||||
}
|
||||
|
||||
type GetUserInviteStatsRequest struct{}
|
||||
|
||||
type GetUserInviteStatsResponse struct {
|
||||
FriendlyCount int64 `json:"friendly_count"`
|
||||
HistoryCount int64 `json:"history_count"`
|
||||
}
|
||||
|
||||
type GetInviteSalesRequest struct {
|
||||
Page int `form:"page" validate:"required"`
|
||||
Size int `form:"size" validate:"required"`
|
||||
StartTime int64 `form:"start_time,optional"`
|
||||
EndTime int64 `form:"end_time,optional"`
|
||||
}
|
||||
|
||||
type GetInviteSalesResponse struct {
|
||||
Total int64 `json:"total"`
|
||||
List []InvitedUserSale `json:"list"`
|
||||
}
|
||||
|
||||
type InvitedUserSale struct {
|
||||
Amount float64 `json:"amount"`
|
||||
UpdatedAt int64 `json:"update_at"`
|
||||
UserHash string `json:"user_hash"`
|
||||
ProductName string `json:"product_name"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user