无订阅 支付后出现两个订阅
Build docker and publish / build (20.15.1) (push) Failing after 7m37s

This commit is contained in:
2026-03-05 21:53:36 -08:00
parent a1ab0fefa4
commit 7308aa9191
51 changed files with 2041 additions and 556 deletions
+3
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
+4 -1
View File
@@ -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")
+137
View File
@@ -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())
}
}
+22 -13
View File
@@ -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 {
+12
View File
@@ -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)
}
+40 -3
View File
@@ -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
}
+10
View File
@@ -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
View File
@@ -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)
+43 -38
View File
@@ -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),
+61
View File
@@ -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
View File
@@ -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"`
}