diff --git a/adapter/adapter.go b/adapter/adapter.go index 1acc674..da5d049 100644 --- a/adapter/adapter.go +++ b/adapter/adapter.go @@ -102,35 +102,53 @@ func (adapter *Adapter) Proxies(servers []*node.Node) ([]Proxy, error) { for _, protocol := range protocols { if protocol.Type == item.Protocol { proxies = append(proxies, Proxy{ - Sort: item.Sort, - Name: item.Name, - Server: item.Address, - Port: item.Port, - Type: item.Protocol, - Tags: strings.Split(item.Tags, ","), - Security: protocol.Security, - SNI: protocol.SNI, - AllowInsecure: protocol.AllowInsecure, - Fingerprint: protocol.Fingerprint, - RealityServerAddr: protocol.RealityServerAddr, - RealityServerPort: protocol.RealityServerPort, - RealityPrivateKey: protocol.RealityPrivateKey, - RealityPublicKey: protocol.RealityPublicKey, - RealityShortId: protocol.RealityShortId, - Transport: protocol.Transport, - Host: protocol.Host, - Path: protocol.Path, - ServiceName: protocol.ServiceName, - Method: protocol.Cipher, - ServerKey: protocol.ServerKey, - Flow: protocol.Flow, - HopPorts: protocol.HopPorts, - HopInterval: protocol.HopInterval, - ObfsPassword: protocol.ObfsPassword, - DisableSNI: protocol.DisableSNI, - ReduceRtt: protocol.ReduceRtt, - UDPRelayMode: protocol.UDPRelayMode, - CongestionController: protocol.CongestionController, + Sort: item.Sort, + Name: item.Name, + Server: item.Address, + Port: item.Port, + Type: item.Protocol, + Tags: strings.Split(item.Tags, ","), + Security: protocol.Security, + SNI: protocol.SNI, + AllowInsecure: protocol.AllowInsecure, + Fingerprint: protocol.Fingerprint, + RealityServerAddr: protocol.RealityServerAddr, + RealityServerPort: protocol.RealityServerPort, + RealityPrivateKey: protocol.RealityPrivateKey, + RealityPublicKey: protocol.RealityPublicKey, + RealityShortId: protocol.RealityShortId, + Transport: protocol.Transport, + Host: protocol.Host, + Path: protocol.Path, + ServiceName: protocol.ServiceName, + Method: protocol.Cipher, + ServerKey: protocol.ServerKey, + Flow: protocol.Flow, + HopPorts: protocol.HopPorts, + HopInterval: protocol.HopInterval, + ObfsPassword: protocol.ObfsPassword, + UpMbps: protocol.UpMbps, + DownMbps: protocol.DownMbps, + DisableSNI: protocol.DisableSNI, + ReduceRtt: protocol.ReduceRtt, + UDPRelayMode: protocol.UDPRelayMode, + CongestionController: protocol.CongestionController, + PaddingScheme: protocol.PaddingScheme, + Multiplex: protocol.Multiplex, + XhttpMode: protocol.XhttpMode, + XhttpExtra: protocol.XhttpExtra, + Encryption: protocol.Encryption, + EncryptionMode: protocol.EncryptionMode, + EncryptionRtt: protocol.EncryptionRtt, + EncryptionTicket: protocol.EncryptionTicket, + EncryptionServerPadding: protocol.EncryptionServerPadding, + EncryptionPrivateKey: protocol.EncryptionPrivateKey, + EncryptionClientPadding: protocol.EncryptionClientPadding, + EncryptionPassword: protocol.EncryptionPassword, + Ratio: protocol.Ratio, + CertMode: protocol.CertMode, + CertDNSProvider: protocol.CertDNSProvider, + CertDNSEnv: protocol.CertDNSEnv, }) } } diff --git a/apis/admin/subscribe.api b/apis/admin/subscribe.api index 1d08b65..bea205d 100644 --- a/apis/admin/subscribe.api +++ b/apis/admin/subscribe.api @@ -102,6 +102,9 @@ type ( BatchDeleteSubscribeRequest { Ids []int64 `json:"ids" validate:"required"` } + ResetAllSubscribeTokenResponse { + Success bool `json:"success"` + } ) @server ( @@ -157,5 +160,9 @@ service ppanel { @doc "Subscribe sort" @handler SubscribeSort post /sort (SubscribeSortRequest) + + @doc "Reset all subscribe tokens" + @handler ResetAllSubscribeToken + post /reset_all_token returns (ResetAllSubscribeTokenResponse) } diff --git a/apis/admin/system.api b/apis/admin/system.api index a300cda..d82d3bd 100644 --- a/apis/admin/system.api +++ b/apis/admin/system.api @@ -19,8 +19,13 @@ type ( Periods []TimePeriod `json:"periods"` } PreViewNodeMultiplierResponse { - CurrentTime string `json:"current_time"` - Ratio float32 `json:"ratio"` + CurrentTime string `json:"current_time"` + Ratio float32 `json:"ratio"` + } + ModuleConfig { + Secret string `json:"secret"` // 通讯密钥 + ServiceName string `json:"service_name"` // 服务名称 + ServiceVersion string `json:"service_version"` // 服务版本 } ) @@ -125,5 +130,9 @@ service ppanel { @doc "PreView Node Multiplier" @handler PreViewNodeMultiplier get /node_multiplier/preview returns (PreViewNodeMultiplierResponse) + + @doc "Get Module Config" + @handler GetModuleConfig + get /module returns (ModuleConfig) } diff --git a/apis/admin/tool.api b/apis/admin/tool.api index da1916b..4d1f17b 100644 --- a/apis/admin/tool.api +++ b/apis/admin/tool.api @@ -17,6 +17,14 @@ type ( VersionResponse { Version string `json:"version"` } + QueryIPLocationRequest { + IP string `form:"ip" validate:"required"` + } + QueryIPLocationResponse { + Country string `json:"country"` + Region string `json:"region,omitempty"` + City string `json:"city"` + } ) @server ( @@ -36,5 +44,9 @@ service ppanel { @doc "Get Version" @handler GetVersion get /version returns (VersionResponse) + + @doc "Query IP Location" + @handler QueryIPLocation + get /ip/location (QueryIPLocationRequest) returns (QueryIPLocationResponse) } diff --git a/apis/auth/auth.api b/apis/auth/auth.api index cb7225d..8211bef 100644 --- a/apis/auth/auth.api +++ b/apis/auth/auth.api @@ -11,11 +11,13 @@ info ( type ( // User login request UserLoginRequest { - Email string `json:"email" validate:"required"` - Password string `json:"password" validate:"required"` - IP string `header:"X-Original-Forwarded-For"` - UserAgent string `header:"User-Agent"` - CfToken string `json:"cf_token,optional"` + Identifier string `json:"identifier"` + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type"` + CfToken string `json:"cf_token,optional"` } // Check user is exist request CheckUserRequest { @@ -27,22 +29,26 @@ type ( } // User login response UserRegisterRequest { - Email string `json:"email" validate:"required"` - Password string `json:"password" validate:"required"` - Invite string `json:"invite,optional"` - Code string `json:"code,optional"` - IP string `header:"X-Original-Forwarded-For"` - UserAgent string `header:"User-Agent"` - CfToken string `json:"cf_token,optional"` + Identifier string `json:"identifier"` + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + Invite string `json:"invite,optional"` + Code string `json:"code,optional"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type"` + CfToken string `json:"cf_token,optional"` } // User login response ResetPasswordRequest { - Email string `json:"email" validate:"required"` - Password string `json:"password" validate:"required"` - Code string `json:"code,optional"` - IP string `header:"X-Original-Forwarded-For"` - UserAgent string `header:"User-Agent"` - CfToken string `json:"cf_token,optional"` + Identifier string `json:"identifier"` + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + Code string `json:"code,optional"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type"` + CfToken string `json:"cf_token,optional"` } LoginResponse { Token string `json:"token"` @@ -60,12 +66,14 @@ type ( } // login request TelephoneLoginRequest { + Identifier string `json:"identifier"` Telephone string `json:"telephone" validate:"required"` TelephoneCode string `json:"telephone_code"` TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` Password string `json:"password"` IP string `header:"X-Original-Forwarded-For"` UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type"` CfToken string `json:"cf_token,optional"` } // Check user is exist request @@ -79,6 +87,7 @@ type ( } // User login response TelephoneRegisterRequest { + Identifier string `json:"identifier"` Telephone string `json:"telephone" validate:"required"` TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` Password string `json:"password" validate:"required"` @@ -86,16 +95,19 @@ type ( Code string `json:"code,optional"` IP string `header:"X-Original-Forwarded-For"` UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type,optional"` CfToken string `json:"cf_token,optional"` } // User login response TelephoneResetPasswordRequest { + Identifier string `json:"identifier"` Telephone string `json:"telephone" validate:"required"` TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` Password string `json:"password" validate:"required"` Code string `json:"code,optional"` IP string `header:"X-Original-Forwarded-For"` UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type,optional"` CfToken string `json:"cf_token,optional"` } AppleLoginCallbackRequest { @@ -107,11 +119,18 @@ type ( Code string `form:"code"` State string `form:"state"` } + DeviceLoginRequest { + Identifier string `json:"identifier" validate:"required"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `json:"user_agent" validate:"required"` + CfToken string `json:"cf_token,optional"` + } ) @server ( - prefix: v1/auth - group: auth + prefix: v1/auth + group: auth + middleware: DeviceMiddleware ) service ppanel { @doc "User login" @@ -145,6 +164,10 @@ service ppanel { @doc "Reset password" @handler TelephoneResetPassword post /reset/telephone (TelephoneResetPasswordRequest) returns (LoginResponse) + + @doc "Device Login" + @handler DeviceLogin + post /login/device (DeviceLoginRequest) returns (LoginResponse) } @server ( diff --git a/apis/common.api b/apis/common.api index 6617a17..db935f4 100644 --- a/apis/common.api +++ b/apis/common.api @@ -87,11 +87,17 @@ type ( Total int64 `json:"total"` List []SubscribeClient `json:"list"` } + HeartbeatResponse { + Status bool `json:"status"` + Message string `json:"message,omitempty"` + Timestamp int64 `json:"timestamp,omitempty"` + } ) @server ( - prefix: v1/common - group: common + prefix: v1/common + group: common + middleware: DeviceMiddleware ) service ppanel { @doc "Get global config" @@ -129,5 +135,9 @@ service ppanel { @doc "Get Client" @handler GetClient get /client returns (GetSubscribeClientResponse) + + @doc "Heartbeat" + @handler Heartbeat + get /heartbeat returns (HeartbeatResponse) } diff --git a/apis/public/announcement.api b/apis/public/announcement.api index 5afd09b..7122e4e 100644 --- a/apis/public/announcement.api +++ b/apis/public/announcement.api @@ -13,7 +13,7 @@ import "../types.api" @server ( prefix: v1/public/announcement group: public/announcement - middleware: AuthMiddleware + middleware: AuthMiddleware,DeviceMiddleware ) service ppanel { @doc "Query announcement" diff --git a/apis/public/document.api b/apis/public/document.api index 4a5e6f9..660bea6 100644 --- a/apis/public/document.api +++ b/apis/public/document.api @@ -13,7 +13,7 @@ import "../types.api" @server ( prefix: v1/public/document group: public/document - middleware: AuthMiddleware + middleware: AuthMiddleware,DeviceMiddleware ) service ppanel { @doc "Get document list" diff --git a/apis/public/order.api b/apis/public/order.api index 0db556f..4e83b0f 100644 --- a/apis/public/order.api +++ b/apis/public/order.api @@ -13,7 +13,7 @@ import "../types.api" @server ( prefix: v1/public/order group: public/order - middleware: AuthMiddleware + middleware: AuthMiddleware,DeviceMiddleware ) service ppanel { @doc "Pre create order" diff --git a/apis/public/payment.api b/apis/public/payment.api index 4876abd..a4893ab 100644 --- a/apis/public/payment.api +++ b/apis/public/payment.api @@ -13,7 +13,7 @@ import "../types.api" @server ( prefix: v1/public/payment group: public/payment - middleware: AuthMiddleware + middleware: AuthMiddleware,DeviceMiddleware ) service ppanel { @doc "Get available payment methods" diff --git a/apis/public/portal.api b/apis/public/portal.api index 33d9948..2d33861 100644 --- a/apis/public/portal.api +++ b/apis/public/portal.api @@ -68,8 +68,9 @@ type ( ) @server ( - prefix: v1/public/portal - group: public/portal + prefix: v1/public/portal + group: public/portal + middleware: DeviceMiddleware ) service ppanel { @doc "Get available payment methods" diff --git a/apis/public/subscribe.api b/apis/public/subscribe.api index c5eaa63..25234ad 100644 --- a/apis/public/subscribe.api +++ b/apis/public/subscribe.api @@ -14,16 +14,54 @@ type ( QuerySubscribeListRequest { Language string `form:"language"` } + QueryUserSubscribeNodeListResponse { + List []UserSubscribeInfo `json:"list"` + } + UserSubscribeInfo { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + OrderId int64 `json:"order_id"` + SubscribeId int64 `json:"subscribe_id"` + StartTime int64 `json:"start_time"` + ExpireTime int64 `json:"expire_time"` + FinishedAt int64 `json:"finished_at"` + ResetTime int64 `json:"reset_time"` + Traffic int64 `json:"traffic"` + Download int64 `json:"download"` + Upload int64 `json:"upload"` + Token string `json:"token"` + Status uint8 `json:"status"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + IsTryOut bool `json:"is_try_out"` + Nodes []*UserSubscribeNodeInfo `json:"nodes"` + } + UserSubscribeNodeInfo { + Id int64 `json:"id"` + Name string `json:"name"` + Uuid string `json:"uuid"` + Protocol string `json:"protocol"` + Port uint16 `json:"port"` + Address string `json:"address"` + Tags []string `json:"tags"` + Country string `json:"country"` + City string `json:"city"` + CreatedAt int64 `json:"created_at"` + } ) @server ( prefix: v1/public/subscribe group: public/subscribe - middleware: AuthMiddleware + middleware: AuthMiddleware,DeviceMiddleware ) service ppanel { @doc "Get subscribe list" @handler QuerySubscribeList get /list (QuerySubscribeListRequest) returns (QuerySubscribeListResponse) + + @doc "Get user subscribe node info" + @handler QueryUserSubscribeNodeList + get /node/list returns (QueryUserSubscribeNodeListResponse) } diff --git a/apis/public/ticket.api b/apis/public/ticket.api index 0f7d2e5..3c6bf70 100644 --- a/apis/public/ticket.api +++ b/apis/public/ticket.api @@ -45,7 +45,7 @@ type ( @server ( prefix: v1/public/ticket group: public/ticket - middleware: AuthMiddleware + middleware: AuthMiddleware,DeviceMiddleware ) service ppanel { @doc "Get ticket list" diff --git a/apis/public/user.api b/apis/public/user.api index 3236bc5..f37eef3 100644 --- a/apis/public/user.api +++ b/apis/public/user.api @@ -97,12 +97,48 @@ type ( Email string `json:"email" validate:"required"` Code string `json:"code" validate:"required"` } + GetDeviceListResponse { + List []UserDevice `json:"list"` + Total int64 `json:"total"` + } + UnbindDeviceRequest { + Id int64 `json:"id" validate:"required"` + } + UpdateUserSubscribeNoteRequest { + UserSubscribeId int64 `json:"user_subscribe_id" validate:"required"` + Note string `json:"note" validate:"max=500"` + } + UpdateUserRulesRequest { + Rules []string `json:"rules" validate:"required"` + } + CommissionWithdrawRequest { + Amount int64 `json:"amount"` + Content string `json:"content"` + } + WithdrawalLog { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + Amount int64 `json:"amount"` + Content string `json:"content"` + Status uint8 `json:"status"` + Reason string `json:"reason,omitempty"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } + QueryWithdrawalLogListRequest { + Page int `form:"page"` + Size int `form:"size"` + } + QueryWithdrawalLogListResponse { + List []WithdrawalLog `json:"list"` + Total int64 `json:"total"` + } ) @server ( prefix: v1/public/user group: public/user - middleware: AuthMiddleware + middleware: AuthMiddleware,DeviceMiddleware ) service ppanel { @doc "Query User Info" @@ -192,5 +228,29 @@ service ppanel { @doc "Update Bind Email" @handler UpdateBindEmail put /bind_email (UpdateBindEmailRequest) + + @doc "Get Device List" + @handler GetDeviceList + get /devices returns (GetDeviceListResponse) + + @doc "Unbind Device" + @handler UnbindDevice + put /unbind_device (UnbindDeviceRequest) + + @doc "Update User Subscribe Note" + @handler UpdateUserSubscribeNote + put /subscribe_note (UpdateUserSubscribeNoteRequest) + + @doc "Update User Rules" + @handler UpdateUserRules + put /rules (UpdateUserRulesRequest) + + @doc "Commission Withdraw" + @handler CommissionWithdraw + post /commission_withdraw (CommissionWithdrawRequest) returns (WithdrawalLog) + + @doc "Query Withdrawal Log" + @handler QueryWithdrawalLog + get /withdrawal_log (QueryWithdrawalLogListRequest) returns (QueryWithdrawalLogListResponse) } diff --git a/apis/types.api b/apis/types.api index 1ae95a6..c6777c2 100644 --- a/apis/types.api +++ b/apis/types.api @@ -28,6 +28,7 @@ type ( EnableTradeNotify bool `json:"enable_trade_notify"` AuthMethods []UserAuthMethod `json:"auth_methods"` UserDevices []UserDevice `json:"user_devices"` + Rules []string `json:"rules"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` DeletedAt int64 `json:"deleted_at,omitempty"` @@ -115,6 +116,7 @@ type ( AuthConfig { Mobile MobileAuthenticateConfig `json:"mobile"` Email EmailAuthticateConfig `json:"email"` + Device DeviceAuthticateConfig `json:"device"` Register PubilcRegisterConfig `json:"register"` } PubilcRegisterConfig { @@ -134,6 +136,12 @@ type ( EnableDomainSuffix bool `json:"enable_domain_suffix"` DomainSuffixList string `json:"domain_suffix_list"` } + DeviceAuthticateConfig { + Enable bool `json:"enable"` + ShowAds bool `json:"show_ads"` + EnableSecurity bool `json:"enable_security"` + OnlyRealDevice bool `json:"only_real_device"` + } RegisterConfig { StopRegister bool `json:"stop_register"` EnableTrial bool `json:"enable_trial"` @@ -464,6 +472,7 @@ type ( Upload int64 `json:"upload"` Token string `json:"token"` Status uint8 `json:"status"` + Short string `json:"short"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } diff --git a/cmd/migrate.go b/cmd/migrate.go new file mode 100644 index 0000000..3b6c76a --- /dev/null +++ b/cmd/migrate.go @@ -0,0 +1,42 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/perfect-panel/server/initialize" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/conf" + "github.com/perfect-panel/server/pkg/logger" + "github.com/spf13/cobra" +) + +func init() { + migrateCmd.Flags().StringVar(&migrateConfigPath, "config", "etc/ppanel.yaml", "ppanel.yaml directory to read from") + rootCmd.AddCommand(migrateCmd) +} + +var ( + migrateConfigPath string +) + +var migrateCmd = &cobra.Command{ + Use: "migrate", + Short: "Run database migrations", + Run: func(cmd *cobra.Command, args []string) { + runMigrate() + }, +} + +func runMigrate() { + var c config.Config + conf.MustLoad(migrateConfigPath, &c) + if err := logger.SetUp(c.Logger); err != nil { + fmt.Println("Logger setup failed:", err.Error()) + os.Exit(1) + } + ctx := svc.NewServiceContext(c) + initialize.Migrate(ctx) + logger.Info("[Migrate] database migration completed") +} diff --git a/go.mod b/go.mod index 04daadd..9d0b191 100644 --- a/go.mod +++ b/go.mod @@ -44,7 +44,7 @@ require ( go.opentelemetry.io/otel/sdk v1.29.0 go.opentelemetry.io/otel/trace v1.29.0 go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.32.0 + golang.org/x/crypto v0.35.0 golang.org/x/oauth2 v0.25.0 golang.org/x/time v0.6.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df @@ -60,6 +60,7 @@ require ( github.com/fatih/color v1.18.0 github.com/goccy/go-json v0.10.4 github.com/golang-migrate/migrate/v4 v4.18.2 + github.com/oschwald/geoip2-golang v1.13.0 github.com/spaolacci/murmur3 v1.1.0 google.golang.org/grpc v1.64.1 google.golang.org/protobuf v1.36.3 @@ -117,6 +118,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/openzipkin/zipkin-go v0.4.2 // indirect + github.com/oschwald/maxminddb-golang v1.13.0 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect @@ -138,8 +140,8 @@ require ( golang.org/x/arch v0.13.0 // indirect golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d // indirect golang.org/x/net v0.34.0 // indirect - golang.org/x/sys v0.29.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect diff --git a/go.sum b/go.sum index 9e9f2c5..aa84f0e 100644 --- a/go.sum +++ b/go.sum @@ -285,6 +285,10 @@ github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQ github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/openzipkin/zipkin-go v0.4.2 h1:zjqfqHjUpPmB3c1GlCvvgsM1G4LkvqQbBDueDOCg/jA= github.com/openzipkin/zipkin-go v0.4.2/go.mod h1:ZeVkFjuuBiSy13y8vpSDCjMi9GoI3hPpCJSBx/EYFhY= +github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNsSFzQATJI= +github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo= +github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU= +github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -402,8 +406,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= +golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d h1:N0hmiNbwsSNwHBAvR3QB5w25pUwH4tK0Y/RltD1j1h4= golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= @@ -463,8 +467,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -478,8 +482,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= diff --git a/initialize/config.go b/initialize/config.go index 025220f..0667543 100644 --- a/initialize/config.go +++ b/initialize/config.go @@ -9,6 +9,7 @@ import ( "net/http" "os" + "github.com/perfect-panel/server/internal/report" "github.com/perfect-panel/server/pkg/logger" "gorm.io/driver/mysql" @@ -35,10 +36,30 @@ func Config(path string) (chan bool, *http.Server) { configPath = path // Create a new Gin instance r := gin.Default() + // get server port + port := 8080 + host := "127.0.0.1" + // check gateway mode + if report.IsGatewayMode() { + // get free port + freePort, err := report.ModulePort() + if err != nil { + logger.Errorf("get module port error: %s", err.Error()) + panic(err) + } + port = freePort + // register module + err = report.RegisterModule(port) + if err != nil { + logger.Errorf("register module error: %s", err.Error()) + panic(err) + } + logger.Infof("module registered on port %d", port) + } // Create a new HTTP server server := &http.Server{ - Addr: ":8080", + Addr: fmt.Sprintf("%s:%d", host, port), Handler: r, } // Load templates diff --git a/initialize/device.go b/initialize/device.go new file mode 100644 index 0000000..1b8c527 --- /dev/null +++ b/initialize/device.go @@ -0,0 +1,26 @@ +package initialize + +import ( + "context" + + "github.com/perfect-panel/server/pkg/logger" + + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/auth" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/tool" +) + +func Device(ctx *svc.ServiceContext) { + logger.Debug("device config initialization") + method, err := ctx.AuthModel.FindOneByMethod(context.Background(), "device") + if err != nil { + panic(err) + } + var cfg config.DeviceConfig + var deviceConfig auth.DeviceConfig + deviceConfig.Unmarshal(method.Config) + tool.DeepCopy(&cfg, deviceConfig) + cfg.Enable = *method.Enabled + ctx.Config.Device = cfg +} diff --git a/initialize/init.go b/initialize/init.go index 02ce905..8023ce5 100644 --- a/initialize/init.go +++ b/initialize/init.go @@ -9,6 +9,7 @@ func StartInitSystemConfig(svc *svc.ServiceContext) { Site(svc) Node(svc) Email(svc) + Device(svc) Invite(svc) Verify(svc) Subscribe(svc) diff --git a/initialize/migrate/database/02115_ads.down.sql b/initialize/migrate/database/02115_ads.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/initialize/migrate/database/02115_ads.up.sql b/initialize/migrate/database/02115_ads.up.sql new file mode 100644 index 0000000..39cfaf2 --- /dev/null +++ b/initialize/migrate/database/02115_ads.up.sql @@ -0,0 +1,20 @@ +-- 只有当 ads 表中不存在 description 字段时才添加 +SET +@col_exists := ( + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'ads' + AND COLUMN_NAME = 'description' +); + +SET +@query := IF( + @col_exists = 0, + 'ALTER TABLE `ads` ADD COLUMN `description` VARCHAR(255) DEFAULT '''' COMMENT ''Description'';', + 'SELECT "Column `description` already exists"' +); + +PREPARE stmt FROM @query; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; diff --git a/initialize/migrate/database/02116_user_algo.down.sql b/initialize/migrate/database/02116_user_algo.down.sql new file mode 100644 index 0000000..41644c2 --- /dev/null +++ b/initialize/migrate/database/02116_user_algo.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE `user` +DROP COLUMN `algo`, + DROP COLUMN `salt`; diff --git a/initialize/migrate/database/02116_user_algo.up.sql b/initialize/migrate/database/02116_user_algo.up.sql new file mode 100644 index 0000000..4a79ef2 --- /dev/null +++ b/initialize/migrate/database/02116_user_algo.up.sql @@ -0,0 +1,35 @@ +-- 添加 algo 列(如果不存在) +SET @dbname = DATABASE(); +SET @tablename = 'user'; +SET @colname = 'algo'; +SET @sql = ( + SELECT IF( + COUNT(*) = 0, + 'ALTER TABLE `user` ADD COLUMN `algo` VARCHAR(20) NOT NULL DEFAULT ''default'' COMMENT ''Encryption Algorithm'' AFTER `password`;', + 'SELECT "Column `algo` already exists";' + ) + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @dbname + AND TABLE_NAME = @tablename + AND COLUMN_NAME = @colname +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- 添加 salt 列(如果不存在) +SET @colname = 'salt'; +SET @sql = ( + SELECT IF( + COUNT(*) = 0, + 'ALTER TABLE `user` ADD COLUMN `salt` VARCHAR(20) NOT NULL DEFAULT ''default'' COMMENT ''Password Salt'' AFTER `algo`;', + 'SELECT "Column `salt` already exists";' + ) + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @dbname + AND TABLE_NAME = @tablename + AND COLUMN_NAME = @colname +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; diff --git a/initialize/migrate/database/02117_site_custom_data.down.sql b/initialize/migrate/database/02117_site_custom_data.down.sql new file mode 100644 index 0000000..c8581e8 --- /dev/null +++ b/initialize/migrate/database/02117_site_custom_data.down.sql @@ -0,0 +1,7 @@ +INSERT INTO `system` (`category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`) +SELECT 'site', 'CustomData', '{ + "kr_website_id": "" +}', 'string', 'Custom Data', '2025-04-22 14:25:16.637', '2025-10-14 15:47:19.187' + WHERE NOT EXISTS ( + SELECT 1 FROM `system` WHERE `category` = 'site' AND `key` = 'CustomData' +); diff --git a/initialize/migrate/database/02117_site_custom_data.up.sql b/initialize/migrate/database/02117_site_custom_data.up.sql new file mode 100644 index 0000000..c8581e8 --- /dev/null +++ b/initialize/migrate/database/02117_site_custom_data.up.sql @@ -0,0 +1,7 @@ +INSERT INTO `system` (`category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`) +SELECT 'site', 'CustomData', '{ + "kr_website_id": "" +}', 'string', 'Custom Data', '2025-04-22 14:25:16.637', '2025-10-14 15:47:19.187' + WHERE NOT EXISTS ( + SELECT 1 FROM `system` WHERE `category` = 'site' AND `key` = 'CustomData' +); diff --git a/initialize/migrate/database/02118_traffic_log_idx.down.sql b/initialize/migrate/database/02118_traffic_log_idx.down.sql new file mode 100644 index 0000000..25598dd --- /dev/null +++ b/initialize/migrate/database/02118_traffic_log_idx.down.sql @@ -0,0 +1 @@ +ALTER TABLE traffic_log DROP INDEX idx_timestamp; diff --git a/initialize/migrate/database/02118_traffic_log_idx.up.sql b/initialize/migrate/database/02118_traffic_log_idx.up.sql new file mode 100644 index 0000000..cdd308f --- /dev/null +++ b/initialize/migrate/database/02118_traffic_log_idx.up.sql @@ -0,0 +1 @@ +ALTER TABLE traffic_log ADD INDEX idx_timestamp (timestamp); diff --git a/initialize/migrate/database/02119_user_subscribe_note.down.sql b/initialize/migrate/database/02119_user_subscribe_note.down.sql new file mode 100644 index 0000000..60cc0e8 --- /dev/null +++ b/initialize/migrate/database/02119_user_subscribe_note.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE `user_subscribe` +DROP COLUMN `note`; diff --git a/initialize/migrate/database/02119_user_subscribe_note.up.sql b/initialize/migrate/database/02119_user_subscribe_note.up.sql new file mode 100644 index 0000000..b8b6983 --- /dev/null +++ b/initialize/migrate/database/02119_user_subscribe_note.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE `user_subscribe` +ADD COLUMN `note` VARCHAR(500) NOT NULL DEFAULT '' + COMMENT 'User note for subscription' + AFTER `status`; diff --git a/initialize/migrate/database/02120_user_rules.down.sql b/initialize/migrate/database/02120_user_rules.down.sql new file mode 100644 index 0000000..718f4a6 --- /dev/null +++ b/initialize/migrate/database/02120_user_rules.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE `user` +DROP COLUMN IF EXISTS `rules`; diff --git a/initialize/migrate/database/02120_user_rules.up.sql b/initialize/migrate/database/02120_user_rules.up.sql new file mode 100644 index 0000000..5e93aca --- /dev/null +++ b/initialize/migrate/database/02120_user_rules.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE `user` + ADD COLUMN `rules` TEXT NULL + COMMENT 'User rules for subscription' + AFTER `created_at`; diff --git a/initialize/migrate/database/02121_user_withdrawal.down.sql b/initialize/migrate/database/02121_user_withdrawal.down.sql new file mode 100644 index 0000000..4de8bc5 --- /dev/null +++ b/initialize/migrate/database/02121_user_withdrawal.down.sql @@ -0,0 +1,5 @@ +DROP TABLE IF EXISTS `withdrawals`; + +DELETE FROM `system` +WHERE `category` = 'invite' + AND `key` = 'WithdrawalMethod'; diff --git a/initialize/migrate/database/02121_user_withdrawal.up.sql b/initialize/migrate/database/02121_user_withdrawal.up.sql new file mode 100644 index 0000000..4f39e1e --- /dev/null +++ b/initialize/migrate/database/02121_user_withdrawal.up.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS `withdrawals` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'Primary Key', + `user_id` BIGINT NOT NULL COMMENT 'User ID', + `amount` BIGINT NOT NULL COMMENT 'Withdrawal Amount', + `content` TEXT COMMENT 'Withdrawal Content', + `status` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Withdrawal Status', + `reason` VARCHAR(500) NOT NULL DEFAULT '' COMMENT 'Rejection Reason', + `created_at` DATETIME NOT NULL COMMENT 'Creation Time', + `updated_at` DATETIME NOT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +INSERT IGNORE INTO `system` (`category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`) +VALUES + ('invite', 'WithdrawalMethod', '', 'string', 'withdrawal method', '2025-04-22 14:25:16.637', '2025-04-22 14:25:16.637'); \ No newline at end of file diff --git a/initialize/version.go b/initialize/version.go index 9fc0527..8f7b8d3 100644 --- a/initialize/version.go +++ b/initialize/version.go @@ -2,6 +2,7 @@ package initialize import ( "errors" + "time" "github.com/perfect-panel/server/internal/model/user" "gorm.io/gorm" @@ -16,13 +17,17 @@ func Migrate(ctx *svc.ServiceContext) { mc := orm.Mysql{ Config: ctx.Config.MySQL, } + now := time.Now() if err := migrate.Migrate(mc.Dsn()).Up(); err != nil { if !errors.Is(err, migrate.NoChange) { logger.Errorf("[Migrate] Up error: %v", err.Error()) panic(err) } logger.Info("[Migrate] database not change") + } else { + logger.Info("[Migrate] Database change, took " + time.Since(now).String()) } + // if not found admin user err := ctx.DB.Transaction(func(tx *gorm.DB) error { var count int64 diff --git a/internal/config/config.go b/internal/config/config.go index 08b59d0..eaadfd5 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,6 +2,7 @@ package config import ( "encoding/json" + "github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/orm" ) @@ -20,6 +21,7 @@ type Config struct { 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"` @@ -95,6 +97,14 @@ type MobileConfig struct { Whitelist []string `yaml:"whitelist"` } +type DeviceConfig struct { + Enable bool `yaml:"enable" default:"true"` + ShowAds bool `yaml:"show_ads"` + EnableSecurity bool `yaml:"enable_security"` + OnlyRealDevice bool `yaml:"only_real_device"` + SecuritySecret string `yaml:"security_secret"` +} + type SiteConfig struct { Host string `yaml:"Host" default:""` SiteName string `yaml:"SiteName" default:""` @@ -190,12 +200,12 @@ type File struct { } type InviteConfig struct { - ForcedInvite bool `yaml:"ForcedInvite" default:"false"` - FirstPurchasePercentage int64 `yaml:"FirstPurchasePercentage" default:"20"` + ForcedInvite bool `yaml:"ForcedInvite" default:"false"` + FirstPurchasePercentage int64 `yaml:"FirstPurchasePercentage" default:"20"` FirstYearlyPurchasePercentage int64 `yaml:"FirstYearlyPurchasePercentage" default:"25"` - NonFirstPurchasePercentage int64 `yaml:"NonFirstPurchasePercentage" default:"10"` - OnlyFirstPurchase bool `yaml:"OnlyFirstPurchase" default:"true"` - ReferralPercentage int64 `yaml:"ReferralPercentage" default:"20"` + NonFirstPurchasePercentage int64 `yaml:"NonFirstPurchasePercentage" default:"10"` + ReferralPercentage uint8 `yaml:"ReferralPercentage" default:"10"` + OnlyFirstPurchase bool `yaml:"OnlyFirstPurchase" default:"false"` } type Telegram struct { diff --git a/internal/handler/admin/subscribe/resetAllSubscribeTokenHandler.go b/internal/handler/admin/subscribe/resetAllSubscribeTokenHandler.go new file mode 100644 index 0000000..408975a --- /dev/null +++ b/internal/handler/admin/subscribe/resetAllSubscribeTokenHandler.go @@ -0,0 +1,18 @@ +package subscribe + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/subscribe" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" +) + +// Reset all subscribe tokens +func ResetAllSubscribeTokenHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := subscribe.NewResetAllSubscribeTokenLogic(c.Request.Context(), svcCtx) + resp, err := l.ResetAllSubscribeToken() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/system/getModuleConfigHandler.go b/internal/handler/admin/system/getModuleConfigHandler.go new file mode 100644 index 0000000..72f87a3 --- /dev/null +++ b/internal/handler/admin/system/getModuleConfigHandler.go @@ -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" +) + +// GetModuleConfigHandler Get Module Config +func GetModuleConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := system.NewGetModuleConfigLogic(c.Request.Context(), svcCtx) + resp, err := l.GetModuleConfig() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/tool/queryIPLocationHandler.go b/internal/handler/admin/tool/queryIPLocationHandler.go new file mode 100644 index 0000000..0b95355 --- /dev/null +++ b/internal/handler/admin/tool/queryIPLocationHandler.go @@ -0,0 +1,26 @@ +package tool + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/tool" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// QueryIPLocationHandler Query IP Location +func QueryIPLocationHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.QueryIPLocationRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := tool.NewQueryIPLocationLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryIPLocation(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/auth/deviceLoginHandler.go b/internal/handler/auth/deviceLoginHandler.go new file mode 100644 index 0000000..6a772bf --- /dev/null +++ b/internal/handler/auth/deviceLoginHandler.go @@ -0,0 +1,26 @@ +package auth + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/auth" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Device Login +func DeviceLoginHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.DeviceLoginRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := auth.NewDeviceLoginLogic(c.Request.Context(), svcCtx) + resp, err := l.DeviceLogin(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/common/getClientHandler.go b/internal/handler/common/getClientHandler.go index e40b555..78d613d 100644 --- a/internal/handler/common/getClientHandler.go +++ b/internal/handler/common/getClientHandler.go @@ -10,7 +10,6 @@ import ( // Get Client func GetClientHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - l := common.NewGetClientLogic(c.Request.Context(), svcCtx) resp, err := l.GetClient() result.HttpResult(c, resp, err) diff --git a/internal/handler/common/heartbeatHandler.go b/internal/handler/common/heartbeatHandler.go new file mode 100644 index 0000000..d72d771 --- /dev/null +++ b/internal/handler/common/heartbeatHandler.go @@ -0,0 +1,18 @@ +package common + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/common" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" +) + +// Heartbeat +func HeartbeatHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := common.NewHeartbeatLogic(c.Request.Context(), svcCtx) + resp, err := l.Heartbeat() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/subscribe/queryUserSubscribeNodeListHandler.go b/internal/handler/public/subscribe/queryUserSubscribeNodeListHandler.go new file mode 100644 index 0000000..16c67fa --- /dev/null +++ b/internal/handler/public/subscribe/queryUserSubscribeNodeListHandler.go @@ -0,0 +1,18 @@ +package subscribe + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/public/subscribe" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" +) + +// Get user subscribe node info +func QueryUserSubscribeNodeListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := subscribe.NewQueryUserSubscribeNodeListLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryUserSubscribeNodeList() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/user/commissionWithdrawHandler.go b/internal/handler/public/user/commissionWithdrawHandler.go new file mode 100644 index 0000000..f4f244c --- /dev/null +++ b/internal/handler/public/user/commissionWithdrawHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Commission Withdraw +func CommissionWithdrawHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CommissionWithdrawRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewCommissionWithdrawLogic(c.Request.Context(), svcCtx) + resp, err := l.CommissionWithdraw(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/user/getDeviceListHandler.go b/internal/handler/public/user/getDeviceListHandler.go new file mode 100644 index 0000000..deb2e3a --- /dev/null +++ b/internal/handler/public/user/getDeviceListHandler.go @@ -0,0 +1,18 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" +) + +// Get Device List +func GetDeviceListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := user.NewGetDeviceListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetDeviceList() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/user/queryWithdrawalLogHandler.go b/internal/handler/public/user/queryWithdrawalLogHandler.go new file mode 100644 index 0000000..9f0bddc --- /dev/null +++ b/internal/handler/public/user/queryWithdrawalLogHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Query Withdrawal Log +func QueryWithdrawalLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.QueryWithdrawalLogListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewQueryWithdrawalLogLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryWithdrawalLog(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/user/unbindDeviceHandler.go b/internal/handler/public/user/unbindDeviceHandler.go new file mode 100644 index 0000000..9429d72 --- /dev/null +++ b/internal/handler/public/user/unbindDeviceHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Unbind Device +func UnbindDeviceHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UnbindDeviceRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewUnbindDeviceLogic(c.Request.Context(), svcCtx) + err := l.UnbindDevice(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/public/user/updateUserRulesHandler.go b/internal/handler/public/user/updateUserRulesHandler.go new file mode 100644 index 0000000..e8b9a01 --- /dev/null +++ b/internal/handler/public/user/updateUserRulesHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Update User Rules +func UpdateUserRulesHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdateUserRulesRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewUpdateUserRulesLogic(c.Request.Context(), svcCtx) + err := l.UpdateUserRules(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/public/user/updateUserSubscribeNoteHandler.go b/internal/handler/public/user/updateUserSubscribeNoteHandler.go new file mode 100644 index 0000000..17b77bf --- /dev/null +++ b/internal/handler/public/user/updateUserSubscribeNoteHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Update User Subscribe Note +func UpdateUserSubscribeNoteHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdateUserSubscribeNoteRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewUpdateUserSubscribeNoteLogic(c.Request.Context(), svcCtx) + err := l.UpdateUserSubscribeNote(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/routes.go b/internal/handler/routes.go index e97fddd..e3ad493 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -385,6 +385,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Get subscribe list adminSubscribeGroupRouter.GET("/list", adminSubscribe.GetSubscribeListHandler(serverCtx)) + // Reset all subscribe tokens + adminSubscribeGroupRouter.POST("/reset_all_token", adminSubscribe.ResetAllSubscribeTokenHandler(serverCtx)) + // Subscribe sort adminSubscribeGroupRouter.POST("/sort", adminSubscribe.SubscribeSortHandler(serverCtx)) } @@ -408,6 +411,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Update invite config adminSystemGroupRouter.PUT("/invite_config", adminSystem.UpdateInviteConfigHandler(serverCtx)) + // Get Module Config + adminSystemGroupRouter.GET("/module", adminSystem.GetModuleConfigHandler(serverCtx)) + // Get node config adminSystemGroupRouter.GET("/node_config", adminSystem.GetNodeConfigHandler(serverCtx)) @@ -487,6 +493,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { adminToolGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) { + // Query IP Location + adminToolGroupRouter.GET("/ip/location", adminTool.QueryIPLocationHandler(serverCtx)) + // Get System Log adminToolGroupRouter.GET("/log", adminTool.GetSystemLogHandler(serverCtx)) @@ -578,6 +587,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { } authGroupRouter := router.Group("/v1/auth") + authGroupRouter.Use(middleware.DeviceMiddleware(serverCtx)) { // Check user is exist @@ -589,6 +599,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // User login authGroupRouter.POST("/login", auth.UserLoginHandler(serverCtx)) + // Device Login + authGroupRouter.POST("/login/device", auth.DeviceLoginHandler(serverCtx)) + // User Telephone login authGroupRouter.POST("/login/telephone", auth.TelephoneLoginHandler(serverCtx)) @@ -619,6 +632,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { } commonGroupRouter := router.Group("/v1/common") + commonGroupRouter.Use(middleware.DeviceMiddleware(serverCtx)) { // Get Ads @@ -630,6 +644,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Get Client commonGroupRouter.GET("/client", common.GetClientHandler(serverCtx)) + // Heartbeat + commonGroupRouter.GET("/heartbeat", common.HeartbeatHandler(serverCtx)) + // Get verification code commonGroupRouter.POST("/send_code", common.SendEmailCodeHandler(serverCtx)) @@ -650,7 +667,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { } publicAnnouncementGroupRouter := router.Group("/v1/public/announcement") - publicAnnouncementGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + publicAnnouncementGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx)) { // Query announcement @@ -658,7 +675,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { } publicDocumentGroupRouter := router.Group("/v1/public/document") - publicDocumentGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + publicDocumentGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx)) { // Get document detail @@ -669,7 +686,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { } publicOrderGroupRouter := router.Group("/v1/public/order") - publicOrderGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + publicOrderGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx)) { // Close order @@ -698,7 +715,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { } publicPaymentGroupRouter := router.Group("/v1/public/payment") - publicPaymentGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + publicPaymentGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx)) { // Get available payment methods @@ -706,6 +723,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { } publicPortalGroupRouter := router.Group("/v1/public/portal") + publicPortalGroupRouter.Use(middleware.DeviceMiddleware(serverCtx)) { // Purchase Checkout @@ -728,15 +746,18 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { } publicSubscribeGroupRouter := router.Group("/v1/public/subscribe") - publicSubscribeGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + publicSubscribeGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx)) { // Get subscribe list publicSubscribeGroupRouter.GET("/list", publicSubscribe.QuerySubscribeListHandler(serverCtx)) + + // Get user subscribe node info + publicSubscribeGroupRouter.GET("/node/list", publicSubscribe.QueryUserSubscribeNodeListHandler(serverCtx)) } publicTicketGroupRouter := router.Group("/v1/public/ticket") - publicTicketGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + publicTicketGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx)) { // Update ticket status @@ -756,7 +777,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { } publicUserGroupRouter := router.Group("/v1/public/user") - publicUserGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + publicUserGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx)) { // Query User Affiliate Count @@ -786,6 +807,12 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Query User Commission Log publicUserGroupRouter.GET("/commission_log", publicUser.QueryUserCommissionLogHandler(serverCtx)) + // Commission Withdraw + publicUserGroupRouter.POST("/commission_withdraw", publicUser.CommissionWithdrawHandler(serverCtx)) + + // Get Device List + publicUserGroupRouter.GET("/devices", publicUser.GetDeviceListHandler(serverCtx)) + // Query User Info publicUserGroupRouter.GET("/info", publicUser.QueryUserInfoHandler(serverCtx)) @@ -801,15 +828,24 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Update User Password publicUserGroupRouter.PUT("/password", publicUser.UpdateUserPasswordHandler(serverCtx)) + // Update User Rules + publicUserGroupRouter.PUT("/rules", publicUser.UpdateUserRulesHandler(serverCtx)) + // Query User Subscribe publicUserGroupRouter.GET("/subscribe", publicUser.QueryUserSubscribeHandler(serverCtx)) // Get Subscribe Log publicUserGroupRouter.GET("/subscribe_log", publicUser.GetSubscribeLogHandler(serverCtx)) + // Update User Subscribe Note + publicUserGroupRouter.PUT("/subscribe_note", publicUser.UpdateUserSubscribeNoteHandler(serverCtx)) + // Reset User Subscribe Token publicUserGroupRouter.PUT("/subscribe_token", publicUser.ResetUserSubscribeTokenHandler(serverCtx)) + // Unbind Device + publicUserGroupRouter.PUT("/unbind_device", publicUser.UnbindDeviceHandler(serverCtx)) + // Unbind OAuth publicUserGroupRouter.POST("/unbind_oauth", publicUser.UnbindOAuthHandler(serverCtx)) @@ -824,6 +860,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Verify Email publicUserGroupRouter.POST("/verify_email", publicUser.VerifyEmailHandler(serverCtx)) + + // Query Withdrawal Log + publicUserGroupRouter.GET("/withdrawal_log", publicUser.QueryWithdrawalLogHandler(serverCtx)) } serverGroupRouter := router.Group("/v1/server") diff --git a/internal/handler/server/queryServerProtocolConfigHandler.go b/internal/handler/server/queryServerProtocolConfigHandler.go index 5d382a6..a2786b9 100644 --- a/internal/handler/server/queryServerProtocolConfigHandler.go +++ b/internal/handler/server/queryServerProtocolConfigHandler.go @@ -36,6 +36,12 @@ func QueryServerProtocolConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Co fmt.Printf("[QueryServerProtocolConfigHandler] - ShouldBindQuery request: %+v\n", req) + if svcCtx.Config.Node.NodeSecret != req.SecretKey { + c.String(http.StatusUnauthorized, "Unauthorized") + c.Abort() + return + } + l := server.NewQueryServerProtocolConfigLogic(c.Request.Context(), svcCtx) resp, err := l.QueryServerProtocolConfig(&req) result.HttpResult(c, resp, err) diff --git a/internal/handler/subscribe.go b/internal/handler/subscribe.go index bf72a19..6c228ed 100644 --- a/internal/handler/subscribe.go +++ b/internal/handler/subscribe.go @@ -23,6 +23,22 @@ func SubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { ua := c.GetHeader("User-Agent") req.UA = c.Request.Header.Get("User-Agent") req.Flag = c.Query("flag") + if svcCtx.Config.Subscribe.PanDomain { + domain := c.Request.Host + domainArr := strings.Split(domain, ".") + short, err := tool.FixedUniqueString(req.Token, 8, "") + if err != nil { + logger.Errorf("[SubscribeHandler] Generate short token failed: %v", err) + c.String(http.StatusInternalServerError, "Internal Server") + c.Abort() + return + } + if short != domainArr[0] { + c.String(http.StatusForbidden, "Access denied") + c.Abort() + return + } + } if svcCtx.Config.Subscribe.UserAgentLimit { if ua == "" { diff --git a/internal/logic/admin/application/previewSubscribeTemplateLogic.go b/internal/logic/admin/application/previewSubscribeTemplateLogic.go index 9bb197d..8ca72e0 100644 --- a/internal/logic/admin/application/previewSubscribeTemplateLogic.go +++ b/internal/logic/admin/application/previewSubscribeTemplateLogic.go @@ -29,10 +29,12 @@ func NewPreviewSubscribeTemplateLogic(ctx context.Context, svcCtx *svc.ServiceCo } func (l *PreviewSubscribeTemplateLogic) PreviewSubscribeTemplate(req *types.PreviewSubscribeTemplateRequest) (resp *types.PreviewSubscribeTemplateResponse, err error) { + enable := true _, servers, err := l.svcCtx.NodeModel.FilterNodeList(l.ctx, &node.FilterNodeParams{ Page: 1, Size: 1000, Preload: true, + Enabled: &enable, }) if err != nil { l.Errorf("[PreviewSubscribeTemplateLogic] FindAllServer error: %v", err.Error()) diff --git a/internal/logic/admin/authMethod/updateAuthMethodConfigLogic.go b/internal/logic/admin/authMethod/updateAuthMethodConfigLogic.go index c20a45f..d61e38f 100644 --- a/internal/logic/admin/authMethod/updateAuthMethodConfigLogic.go +++ b/internal/logic/admin/authMethod/updateAuthMethodConfigLogic.go @@ -92,6 +92,9 @@ func (l *UpdateAuthMethodConfigLogic) UpdateGlobal(method string) { if method == "mobile" { initialize.Mobile(l.svcCtx) } + if method == "device" { + initialize.Device(l.svcCtx) + } } func validatePlatformConfig(platform string, cfg map[string]interface{}) (interface{}, error) { diff --git a/internal/logic/admin/payment/getPaymentMethodListLogic.go b/internal/logic/admin/payment/getPaymentMethodListLogic.go index 77c7e40..ef987f3 100644 --- a/internal/logic/admin/payment/getPaymentMethodListLogic.go +++ b/internal/logic/admin/payment/getPaymentMethodListLogic.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" + "github.com/perfect-panel/server/internal/report" paymentPlatform "github.com/perfect-panel/server/pkg/payment" "github.com/perfect-panel/server/internal/model/payment" @@ -43,15 +44,31 @@ func (l *GetPaymentMethodListLogic) GetPaymentMethodList(req *types.GetPaymentMe Total: total, List: make([]types.PaymentMethodDetail, len(list)), } + + // gateway mod + + isGatewayMod := report.IsGatewayMode() + for i, v := range list { config := make(map[string]interface{}) _ = json.Unmarshal([]byte(v.Config), &config) notifyUrl := "" + if paymentPlatform.ParsePlatform(v.Platform) != paymentPlatform.Balance { + notifyUrl = v.Domain if v.Domain != "" { - notifyUrl = v.Domain + "/v1/notify/" + v.Platform + "/" + v.Token + // if is gateway mod, use gateway domain + if isGatewayMod { + notifyUrl += "/api/" + } + notifyUrl += "/v1/notify/" + v.Platform + "/" + v.Token } else { - notifyUrl = "https://" + l.svcCtx.Config.Host + "/v1/notify/" + v.Platform + "/" + v.Token + notifyUrl += "https://" + l.svcCtx.Config.Host + if isGatewayMod { + notifyUrl += "/api/v1/notify/" + v.Platform + "/" + v.Token + } else { + notifyUrl += "/v1/notify/" + v.Platform + "/" + v.Token + } } } resp.List[i] = types.PaymentMethodDetail{ diff --git a/internal/logic/admin/subscribe/resetAllSubscribeTokenLogic.go b/internal/logic/admin/subscribe/resetAllSubscribeTokenLogic.go new file mode 100644 index 0000000..e7307a2 --- /dev/null +++ b/internal/logic/admin/subscribe/resetAllSubscribeTokenLogic.go @@ -0,0 +1,61 @@ +package subscribe + +import ( + "context" + "strconv" + "time" + + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/pkg/uuidx" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" +) + +type ResetAllSubscribeTokenLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Reset all subscribe tokens +func NewResetAllSubscribeTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetAllSubscribeTokenLogic { + return &ResetAllSubscribeTokenLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ResetAllSubscribeTokenLogic) ResetAllSubscribeToken() (resp *types.ResetAllSubscribeTokenResponse, err error) { + var list []*user.Subscribe + tx := l.svcCtx.DB.WithContext(l.ctx).Begin() + // select all active and Finished subscriptions + if err = tx.Model(&user.Subscribe{}).Where("`status` IN ?", []int64{1, 2}).Find(&list).Error; err != nil { + logger.Errorf("[ResetAllSubscribeToken] Failed to fetch subscribe list: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Failed to fetch subscribe list: %v", err.Error()) + } + + for _, sub := range list { + sub.Token = uuidx.SubscribeToken(strconv.FormatInt(time.Now().UnixMilli(), 10) + strconv.FormatInt(sub.Id, 10)) + sub.UUID = uuidx.NewUUID().String() + if err = tx.Model(&user.Subscribe{}).Where("id = ?", sub.Id).Save(sub).Error; err != nil { + tx.Rollback() + logger.Errorf("[ResetAllSubscribeToken] Failed to update subscribe token for ID %d: %v", sub.Id, err.Error()) + return &types.ResetAllSubscribeTokenResponse{ + Success: false, + }, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "Failed to update subscribe token for ID %d: %v", sub.Id, err.Error()) + } + } + if err = tx.Commit().Error; err != nil { + logger.Errorf("[ResetAllSubscribeToken] Failed to commit transaction: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "Failed to commit transaction: %v", err.Error()) + } + + return &types.ResetAllSubscribeTokenResponse{ + Success: true, + }, nil +} diff --git a/internal/logic/admin/system/getModuleConfigLogic.go b/internal/logic/admin/system/getModuleConfigLogic.go new file mode 100644 index 0000000..2dc646f --- /dev/null +++ b/internal/logic/admin/system/getModuleConfigLogic.go @@ -0,0 +1,41 @@ +package system + +import ( + "context" + "os" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetModuleConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get Module Config +func NewGetModuleConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetModuleConfigLogic { + return &GetModuleConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetModuleConfigLogic) GetModuleConfig() (resp *types.ModuleConfig, err error) { + value, exists := os.LookupEnv("SECRET_KEY") + if !exists { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), " SECRET_KEY not set in environment variables") + } + + return &types.ModuleConfig{ + Secret: value, + ServiceName: constant.ServiceName, + ServiceVersion: constant.Version, + }, nil +} diff --git a/internal/logic/admin/tool/queryIPLocationLogic.go b/internal/logic/admin/tool/queryIPLocationLogic.go new file mode 100644 index 0000000..6487b05 --- /dev/null +++ b/internal/logic/admin/tool/queryIPLocationLogic.go @@ -0,0 +1,57 @@ +package tool + +import ( + "context" + "net" + + "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/xerr" + "github.com/pkg/errors" +) + +type QueryIPLocationLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewQueryIPLocationLogic Query IP Location +func NewQueryIPLocationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryIPLocationLogic { + return &QueryIPLocationLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryIPLocationLogic) QueryIPLocation(req *types.QueryIPLocationRequest) (resp *types.QueryIPLocationResponse, err error) { + if l.svcCtx.GeoIP == nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), " GeoIP database not configured") + } + + ip := net.ParseIP(req.IP) + record, err := l.svcCtx.GeoIP.DB.City(ip) + if err != nil { + l.Errorf("Failed to query IP location: %v", err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Failed to query IP location") + } + + var country, region, city string + if record.Country.Names != nil { + country = record.Country.Names["en"] + } + if len(record.Subdivisions) > 0 && record.Subdivisions[0].Names != nil { + region = record.Subdivisions[0].Names["en"] + } + if record.City.Names != nil { + city = record.City.Names["en"] + } + + return &types.QueryIPLocationResponse{ + Country: country, + Region: region, + City: city, + }, nil +} diff --git a/internal/logic/admin/user/createUserLogic.go b/internal/logic/admin/user/createUserLogic.go index 5f6c858..0fb6b43 100644 --- a/internal/logic/admin/user/createUserLogic.go +++ b/internal/logic/admin/user/createUserLogic.go @@ -40,6 +40,7 @@ func (l *CreateUserLogic) CreateUser(req *types.CreateUserRequest) error { pwd := tool.EncodePassWord(req.Password) newUser := &user.User{ Password: pwd, + Algo: "default", ReferralPercentage: req.ReferralPercentage, OnlyFirstPurchase: &req.OnlyFirstPurchase, ReferCode: req.ReferCode, diff --git a/internal/logic/admin/user/updateUserBasicInfoLogic.go b/internal/logic/admin/user/updateUserBasicInfoLogic.go index 9f57f75..faa7930 100644 --- a/internal/logic/admin/user/updateUserBasicInfoLogic.go +++ b/internal/logic/admin/user/updateUserBasicInfoLogic.go @@ -129,6 +129,7 @@ func (l *UpdateUserBasicInfoLogic) UpdateUserBasicInfo(req *types.UpdateUserBasi return errors.Wrapf(xerr.NewErrCodeMsg(503, "Demo mode does not allow modification of the admin user password"), "UpdateUserBasicInfo failed: cannot update admin user password in demo mode") } userInfo.Password = tool.EncodePassWord(req.Password) + userInfo.Algo = "default" } err = l.svcCtx.UserModel.Update(l.ctx, userInfo) diff --git a/internal/logic/auth/bindDeviceLogic.go b/internal/logic/auth/bindDeviceLogic.go new file mode 100644 index 0000000..19b0666 --- /dev/null +++ b/internal/logic/auth/bindDeviceLogic.go @@ -0,0 +1,234 @@ +package auth + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type BindDeviceLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewBindDeviceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BindDeviceLogic { + return &BindDeviceLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// BindDeviceToUser binds a device to a user +// If the device is already bound to another user, it will disable that user and bind the device to the current user +func (l *BindDeviceLogic) BindDeviceToUser(identifier, ip, userAgent string, currentUserId int64) error { + if identifier == "" { + // No device identifier provided, skip binding + return nil + } + + l.Infow("binding device to user", + logger.Field("identifier", identifier), + logger.Field("user_id", currentUserId), + logger.Field("ip", ip), + ) + + // Check if device exists + deviceInfo, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, identifier) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + // Device not found, create new device record + return l.createDeviceForUser(identifier, ip, userAgent, currentUserId) + } + l.Errorw("failed to query device", + logger.Field("identifier", identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query device failed: %v", err.Error()) + } + + // Device exists, check if it's bound to current user + if deviceInfo.UserId == currentUserId { + // Already bound to current user, just update IP and UserAgent + l.Infow("device already bound to current user, updating info", + logger.Field("identifier", identifier), + logger.Field("user_id", currentUserId), + ) + deviceInfo.Ip = ip + deviceInfo.UserAgent = userAgent + if err := l.svcCtx.UserModel.UpdateDevice(l.ctx, deviceInfo); err != nil { + l.Errorw("failed to update device", + logger.Field("identifier", identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device failed: %v", err.Error()) + } + return nil + } + + // Device is bound to another user, need to disable old user and rebind + l.Infow("device bound to another user, rebinding", + logger.Field("identifier", identifier), + logger.Field("old_user_id", deviceInfo.UserId), + logger.Field("new_user_id", currentUserId), + ) + + return l.rebindDeviceToNewUser(deviceInfo, ip, userAgent, currentUserId) +} + +func (l *BindDeviceLogic) createDeviceForUser(identifier, ip, userAgent string, userId int64) error { + l.Infow("creating new device for user", + logger.Field("identifier", identifier), + logger.Field("user_id", userId), + ) + + err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + // Create device auth method + authMethod := &user.AuthMethods{ + UserId: userId, + AuthType: "device", + AuthIdentifier: identifier, + Verified: true, + } + if err := db.Create(authMethod).Error; err != nil { + l.Errorw("failed to create device auth method", + logger.Field("user_id", userId), + logger.Field("identifier", identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create device auth method failed: %v", err) + } + + // Create device record + deviceInfo := &user.Device{ + Ip: ip, + UserId: userId, + UserAgent: userAgent, + Identifier: identifier, + Enabled: true, + Online: false, + } + if err := db.Create(deviceInfo).Error; err != nil { + l.Errorw("failed to create device", + logger.Field("user_id", userId), + logger.Field("identifier", identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create device failed: %v", err) + } + + return nil + }) + + if err != nil { + l.Errorw("device creation failed", + logger.Field("identifier", identifier), + logger.Field("user_id", userId), + logger.Field("error", err.Error()), + ) + return err + } + + l.Infow("device created successfully", + logger.Field("identifier", identifier), + logger.Field("user_id", userId), + ) + + return nil +} + +func (l *BindDeviceLogic) rebindDeviceToNewUser(deviceInfo *user.Device, ip, userAgent string, newUserId int64) error { + oldUserId := deviceInfo.UserId + + err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + // Check if old user has other auth methods besides device + var authMethods []user.AuthMethods + if err := db.Where("user_id = ?", oldUserId).Find(&authMethods).Error; err != nil { + l.Errorw("failed to query auth methods for old user", + logger.Field("old_user_id", oldUserId), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query auth methods failed: %v", err) + } + + // Count non-device auth methods + nonDeviceAuthCount := 0 + for _, auth := range authMethods { + if auth.AuthType != "device" { + nonDeviceAuthCount++ + } + } + + // Only disable old user if they have no other auth methods + if nonDeviceAuthCount == 0 { + falseVal := false + if err := db.Model(&user.User{}).Where("id = ?", oldUserId).Update("enable", &falseVal).Error; err != nil { + l.Errorw("failed to disable old user", + logger.Field("old_user_id", oldUserId), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "disable old user failed: %v", err) + } + + l.Infow("disabled old user (no other auth methods)", + logger.Field("old_user_id", oldUserId), + ) + } else { + l.Infow("old user has other auth methods, not disabling", + logger.Field("old_user_id", oldUserId), + logger.Field("non_device_auth_count", nonDeviceAuthCount), + ) + } + + // Update device auth method to new user + if err := db.Model(&user.AuthMethods{}). + Where("auth_type = ? AND auth_identifier = ?", "device", deviceInfo.Identifier). + Update("user_id", newUserId).Error; err != nil { + l.Errorw("failed to update device auth method", + logger.Field("identifier", deviceInfo.Identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device auth method failed: %v", err) + } + + // Update device record + deviceInfo.UserId = newUserId + deviceInfo.Ip = ip + deviceInfo.UserAgent = userAgent + deviceInfo.Enabled = true + + if err := db.Save(deviceInfo).Error; err != nil { + l.Errorw("failed to update device", + logger.Field("identifier", deviceInfo.Identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device failed: %v", err) + } + + return nil + }) + + if err != nil { + l.Errorw("device rebinding failed", + logger.Field("identifier", deviceInfo.Identifier), + logger.Field("old_user_id", oldUserId), + logger.Field("new_user_id", newUserId), + logger.Field("error", err.Error()), + ) + return err + } + + l.Infow("device rebound successfully", + logger.Field("identifier", deviceInfo.Identifier), + logger.Field("old_user_id", oldUserId), + logger.Field("new_user_id", newUserId), + ) + + return nil +} diff --git a/internal/logic/auth/deviceLoginLogic.go b/internal/logic/auth/deviceLoginLogic.go new file mode 100644 index 0000000..2e2b6ae --- /dev/null +++ b/internal/logic/auth/deviceLoginLogic.go @@ -0,0 +1,293 @@ +package auth + +import ( + "context" + "fmt" + "time" + + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/jwt" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/uuidx" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type DeviceLoginLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Device Login +func NewDeviceLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeviceLoginLogic { + return &DeviceLoginLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *types.LoginResponse, err error) { + if !l.svcCtx.Config.Device.Enable { + return nil, xerr.NewErrMsg("Device login is disabled") + } + + loginStatus := false + var userInfo *user.User + // Record login status + defer func() { + if userInfo != nil && userInfo.Id != 0 { + loginLog := log.Login{ + Method: "device", + LoginIP: req.IP, + UserAgent: req.UserAgent, + Success: loginStatus, + Timestamp: time.Now().UnixMilli(), + } + content, _ := loginLog.Marshal() + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Type: log.TypeLogin.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: userInfo.Id, + Content: string(content), + }); err != nil { + l.Errorw("failed to insert login log", + logger.Field("user_id", userInfo.Id), + logger.Field("ip", req.IP), + logger.Field("error", err.Error()), + ) + } + } + }() + + // Check if device exists by identifier + deviceInfo, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + // Device not found, create new user and device + userInfo, err = l.registerUserAndDevice(req) + if err != nil { + return nil, err + } + } else { + l.Errorw("query device failed", + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query device failed: %v", err.Error()) + } + } else { + // Device found, get user info + userInfo, err = l.svcCtx.UserModel.FindOne(l.ctx, deviceInfo.UserId) + if err != nil { + l.Errorw("query user failed", + logger.Field("user_id", deviceInfo.UserId), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user failed: %v", err.Error()) + } + } + + // Generate session id + sessionId := uuidx.NewUUID().String() + + // Generate token + token, err := jwt.NewJwtToken( + l.svcCtx.Config.JwtAuth.AccessSecret, + time.Now().Unix(), + l.svcCtx.Config.JwtAuth.AccessExpire, + jwt.WithOption("UserId", userInfo.Id), + jwt.WithOption("SessionId", sessionId), + jwt.WithOption("LoginType", "device"), + ) + if err != nil { + l.Errorw("token generate error", + logger.Field("user_id", userInfo.Id), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error()) + } + + // Store session id in redis + sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) + if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil { + l.Errorw("set session id error", + logger.Field("user_id", userInfo.Id), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error()) + } + + loginStatus = true + return &types.LoginResponse{ + Token: token, + }, nil +} + +func (l *DeviceLoginLogic) registerUserAndDevice(req *types.DeviceLoginRequest) (*user.User, error) { + l.Infow("device not found, creating new user and device", + logger.Field("identifier", req.Identifier), + logger.Field("ip", req.IP), + ) + + var userInfo *user.User + err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + // Create new user + userInfo = &user.User{ + OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase, + } + if err := db.Create(userInfo).Error; err != nil { + l.Errorw("failed to create user", + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create user failed: %v", err) + } + + // Update refer code + userInfo.ReferCode = uuidx.UserInviteCode(userInfo.Id) + if err := db.Model(&user.User{}).Where("id = ?", userInfo.Id).Update("refer_code", userInfo.ReferCode).Error; err != nil { + l.Errorw("failed to update refer code", + logger.Field("user_id", userInfo.Id), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update refer code failed: %v", err) + } + + // Create device auth method + authMethod := &user.AuthMethods{ + UserId: userInfo.Id, + AuthType: "device", + AuthIdentifier: req.Identifier, + Verified: true, + } + if err := db.Create(authMethod).Error; err != nil { + l.Errorw("failed to create device auth method", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create device auth method failed: %v", err) + } + + // Insert device record + deviceInfo := &user.Device{ + Ip: req.IP, + UserId: userInfo.Id, + UserAgent: req.UserAgent, + Identifier: req.Identifier, + Enabled: true, + Online: false, + } + if err := db.Create(deviceInfo).Error; err != nil { + l.Errorw("failed to insert device", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert device failed: %v", err) + } + + // Activate trial if enabled + if l.svcCtx.Config.Register.EnableTrial { + if err := l.activeTrial(userInfo.Id, db); err != nil { + return err + } + } + + return nil + }) + + if err != nil { + l.Errorw("device registration failed", + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + return nil, err + } + + l.Infow("device registration completed successfully", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("refer_code", userInfo.ReferCode), + ) + + // Register log + registerLog := log.Register{ + AuthMethod: "device", + Identifier: req.Identifier, + RegisterIP: req.IP, + UserAgent: req.UserAgent, + Timestamp: time.Now().UnixMilli(), + } + content, _ := registerLog.Marshal() + + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Type: log.TypeRegister.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: userInfo.Id, + Content: string(content), + }); err != nil { + l.Errorw("failed to insert register log", + logger.Field("user_id", userInfo.Id), + logger.Field("ip", req.IP), + logger.Field("error", err.Error()), + ) + } + + return userInfo, nil +} + +func (l *DeviceLoginLogic) activeTrial(userId int64, db *gorm.DB) error { + sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, l.svcCtx.Config.Register.TrialSubscribe) + if err != nil { + l.Errorw("failed to find trial subscription template", + logger.Field("user_id", userId), + logger.Field("trial_subscribe_id", l.svcCtx.Config.Register.TrialSubscribe), + logger.Field("error", err.Error()), + ) + return err + } + + startTime := time.Now() + expireTime := tool.AddTime(l.svcCtx.Config.Register.TrialTimeUnit, l.svcCtx.Config.Register.TrialTime, startTime) + subscribeToken := uuidx.SubscribeToken(fmt.Sprintf("Trial-%v", userId)) + subscribeUUID := uuidx.NewUUID().String() + + userSub := &user.Subscribe{ + UserId: userId, + OrderId: 0, + SubscribeId: sub.Id, + StartTime: startTime, + ExpireTime: expireTime, + Traffic: sub.Traffic, + Download: 0, + Upload: 0, + Token: subscribeToken, + UUID: subscribeUUID, + Status: 1, + } + + if err := db.Create(userSub).Error; err != nil { + l.Errorw("failed to insert trial subscription", + logger.Field("user_id", userId), + logger.Field("error", err.Error()), + ) + return err + } + + l.Infow("trial subscription activated successfully", + logger.Field("user_id", userId), + logger.Field("subscribe_id", sub.Id), + logger.Field("expire_time", expireTime), + logger.Field("traffic", sub.Traffic), + ) + + return nil +} diff --git a/internal/logic/auth/resetPasswordLogic.go b/internal/logic/auth/resetPasswordLogic.go index d0d3f2f..aef245a 100644 --- a/internal/logic/auth/resetPasswordLogic.go +++ b/internal/logic/auth/resetPasswordLogic.go @@ -104,9 +104,26 @@ func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordRequest) (res // Update password userInfo.Password = tool.EncodePassWord(req.Password) - if err := l.svcCtx.UserModel.Update(l.ctx, userInfo); err != nil { + userInfo.Algo = "default" + if err = l.svcCtx.UserModel.Update(l.ctx, userInfo); err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update user info failed: %v", err.Error()) } + + // Bind device to user if identifier is provided + if req.Identifier != "" { + bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx) + if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil { + l.Errorw("failed to bind device to user", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + // Don't fail register if device binding fails, just log the error + } + } + if l.ctx.Value(constant.LoginType) != nil { + req.LoginType = l.ctx.Value(constant.LoginType).(string) + } // Generate session id sessionId := uuidx.NewUUID().String() // Generate token @@ -116,6 +133,7 @@ func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordRequest) (res l.svcCtx.Config.JwtAuth.AccessExpire, jwt.WithOption("UserId", userInfo.Id), jwt.WithOption("SessionId", sessionId), + jwt.WithOption("LoginType", req.LoginType), ) if err != nil { l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) diff --git a/internal/logic/auth/telephoneLoginLogic.go b/internal/logic/auth/telephoneLoginLogic.go index 54e4d8e..8a54ff5 100644 --- a/internal/logic/auth/telephoneLoginLogic.go +++ b/internal/logic/auth/telephoneLoginLogic.go @@ -98,7 +98,7 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r if req.TelephoneCode == "" { // Verify password - if !tool.VerifyPassWord(req.Password, userInfo.Password) { + if !tool.MultiPasswordVerify(userInfo.Algo, userInfo.Salt, req.Password, userInfo.Password) { return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserPasswordError), "user password") } } else { @@ -124,6 +124,23 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r l.svcCtx.Redis.Del(l.ctx, cacheKey) } + // Bind device to user if identifier is provided + if req.Identifier != "" { + bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx) + if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil { + l.Errorw("failed to bind device to user", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + // Don't fail login if device binding fails, just log the error + } + } + + if l.ctx.Value(constant.LoginType) != nil { + req.LoginType = l.ctx.Value(constant.LoginType).(string) + } + // Generate session id sessionId := uuidx.NewUUID().String() // Generate token @@ -133,6 +150,7 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r l.svcCtx.Config.JwtAuth.AccessExpire, jwt.WithOption("UserId", userInfo.Id), jwt.WithOption("SessionId", sessionId), + jwt.WithOption("LoginType", req.LoginType), ) if err != nil { l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) diff --git a/internal/logic/auth/telephoneResetPasswordLogic.go b/internal/logic/auth/telephoneResetPasswordLogic.go index 18891b0..5cb47cc 100644 --- a/internal/logic/auth/telephoneResetPasswordLogic.go +++ b/internal/logic/auth/telephoneResetPasswordLogic.go @@ -78,11 +78,27 @@ func (l *TelephoneResetPasswordLogic) TelephoneResetPassword(req *types.Telephon // Generate password pwd := tool.EncodePassWord(req.Password) userInfo.Password = pwd + userInfo.Algo = "default" err = l.svcCtx.UserModel.Update(l.ctx, userInfo) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "update user password failed: %v", err.Error()) } + // Bind device to user if identifier is provided + if req.Identifier != "" { + bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx) + if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil { + l.Errorw("failed to bind device to user", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + // Don't fail register if device binding fails, just log the error + } + } + if l.ctx.Value(constant.LoginType) != nil { + req.LoginType = l.ctx.Value(constant.LoginType).(string) + } // Generate session id sessionId := uuidx.NewUUID().String() // Generate token @@ -92,6 +108,7 @@ func (l *TelephoneResetPasswordLogic) TelephoneResetPassword(req *types.Telephon l.svcCtx.Config.JwtAuth.AccessExpire, jwt.WithOption("UserId", userInfo.Id), jwt.WithOption("SessionId", sessionId), + jwt.WithOption("LoginType", req.LoginType), ) if err != nil { l.Errorw("[UserLogin] token generate error", logger.Field("error", err.Error())) diff --git a/internal/logic/auth/telephoneUserRegisterLogic.go b/internal/logic/auth/telephoneUserRegisterLogic.go index 6db07ee..af16811 100644 --- a/internal/logic/auth/telephoneUserRegisterLogic.go +++ b/internal/logic/auth/telephoneUserRegisterLogic.go @@ -107,6 +107,7 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR pwd := tool.EncodePassWord(req.Password) userInfo := &user.User{ Password: pwd, + Algo: "default", OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase, AuthMethods: []user.AuthMethods{ { @@ -138,6 +139,22 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR } return nil }) + + // Bind device to user if identifier is provided + if req.Identifier != "" { + bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx) + if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil { + l.Errorw("failed to bind device to user", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + // Don't fail register if device binding fails, just log the error + } + } + if l.ctx.Value(constant.LoginType) != nil { + req.LoginType = l.ctx.Value(constant.LoginType).(string) + } // Generate session id sessionId := uuidx.NewUUID().String() // Generate token @@ -147,6 +164,7 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR l.svcCtx.Config.JwtAuth.AccessExpire, jwt.WithOption("UserId", userInfo.Id), jwt.WithOption("SessionId", sessionId), + jwt.WithOption("LoginType", req.LoginType), ) if err != nil { l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) diff --git a/internal/logic/auth/userLoginLogic.go b/internal/logic/auth/userLoginLogic.go index d328924..deecaae 100644 --- a/internal/logic/auth/userLoginLogic.go +++ b/internal/logic/auth/userLoginLogic.go @@ -6,6 +6,7 @@ import ( "time" "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/internal/config" @@ -76,9 +77,25 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log } // Verify password - if !tool.VerifyPassWord(req.Password, userInfo.Password) { + if !tool.MultiPasswordVerify(userInfo.Algo, userInfo.Salt, req.Password, userInfo.Password) { return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserPasswordError), "user password") } + + // Bind device to user if identifier is provided + if req.Identifier != "" { + bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx) + if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil { + l.Errorw("failed to bind device to user", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + // Don't fail login if device binding fails, just log the error + } + } + if l.ctx.Value(constant.LoginType) != nil { + req.LoginType = l.ctx.Value(constant.LoginType).(string) + } // Generate session id sessionId := uuidx.NewUUID().String() // Generate token @@ -88,6 +105,7 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log l.svcCtx.Config.JwtAuth.AccessExpire, jwt.WithOption("UserId", userInfo.Id), jwt.WithOption("SessionId", sessionId), + jwt.WithOption("LoginType", req.LoginType), ) if err != nil { l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) diff --git a/internal/logic/auth/userRegisterLogic.go b/internal/logic/auth/userRegisterLogic.go index e4e01e4..cf959a9 100644 --- a/internal/logic/auth/userRegisterLogic.go +++ b/internal/logic/auth/userRegisterLogic.go @@ -90,6 +90,7 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp * pwd := tool.EncodePassWord(req.Password) userInfo := &user.User{ Password: pwd, + Algo: "default", OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase, } if referer != nil { @@ -125,6 +126,21 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp * } return nil }) + // Bind device to user if identifier is provided + if req.Identifier != "" { + bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx) + if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil { + l.Errorw("failed to bind device to user", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + // Don't fail register if device binding fails, just log the error + } + } + if l.ctx.Value(constant.LoginType) != nil { + req.LoginType = l.ctx.Value(constant.LoginType).(string) + } // Generate session id sessionId := uuidx.NewUUID().String() // Generate token @@ -134,6 +150,7 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp * l.svcCtx.Config.JwtAuth.AccessExpire, jwt.WithOption("UserId", userInfo.Id), jwt.WithOption("SessionId", sessionId), + jwt.WithOption("LoginType", req.LoginType), ) if err != nil { l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) diff --git a/internal/logic/common/getGlobalConfigLogic.go b/internal/logic/common/getGlobalConfigLogic.go index 694520d..61b2c1e 100644 --- a/internal/logic/common/getGlobalConfigLogic.go +++ b/internal/logic/common/getGlobalConfigLogic.go @@ -2,6 +2,7 @@ package common import ( "context" + "encoding/json" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" @@ -67,6 +68,10 @@ func (l *GetGlobalConfigLogic) GetGlobalConfig() (resp *types.GetGlobalConfigRes for _, method := range authMethods { if *method.Enabled { methods = append(methods, method.Method) + if method.Method == "device" { + _ = json.Unmarshal([]byte(method.Config), &resp.Auth.Device) + resp.Auth.Device.Enable = true + } } } resp.OAuthMethods = methods diff --git a/internal/logic/common/heartbeatLogic.go b/internal/logic/common/heartbeatLogic.go new file mode 100644 index 0000000..1bfb081 --- /dev/null +++ b/internal/logic/common/heartbeatLogic.go @@ -0,0 +1,33 @@ +package common + +import ( + "context" + "time" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" +) + +type HeartbeatLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewHeartbeatLogic Heartbeat +func NewHeartbeatLogic(ctx context.Context, svcCtx *svc.ServiceContext) *HeartbeatLogic { + return &HeartbeatLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *HeartbeatLogic) Heartbeat() (resp *types.HeartbeatResponse, err error) { + return &types.HeartbeatResponse{ + Status: true, + Message: "service is alive", + Timestamp: time.Now().Unix(), + }, nil +} diff --git a/internal/logic/notify/ePayNotifyLogic.go b/internal/logic/notify/ePayNotifyLogic.go index 8def591..efdd127 100644 --- a/internal/logic/notify/ePayNotifyLogic.go +++ b/internal/logic/notify/ePayNotifyLogic.go @@ -57,7 +57,7 @@ func (l *EPayNotifyLogic) EPayNotify(req *types.EPayNotifyRequest) error { return err } // Verify sign - client := epay.NewClient(config.Pid, config.Url, config.Key) + client := epay.NewClient(config.Pid, config.Url, config.Key, config.Type) if !client.VerifySign(urlParamsToMap(l.ctx.Request.URL.RawQuery)) && !l.svcCtx.Config.Debug { l.Logger.Error("[EPayNotify] Verify sign failed") return nil diff --git a/internal/logic/public/portal/purchaseCheckoutLogic.go b/internal/logic/public/portal/purchaseCheckoutLogic.go index f72ed4f..ff7faf2 100644 --- a/internal/logic/public/portal/purchaseCheckoutLogic.go +++ b/internal/logic/public/portal/purchaseCheckoutLogic.go @@ -7,6 +7,7 @@ import ( "time" "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/report" "github.com/perfect-panel/server/pkg/constant" paymentPlatform "github.com/perfect-panel/server/pkg/payment" @@ -267,7 +268,7 @@ func (l *PurchaseCheckoutLogic) epayPayment(config *payment.Payment, info *order return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error()) } // Initialize EPay client with merchant credentials - client := epay.NewClient(epayConfig.Pid, epayConfig.Url, epayConfig.Key) + client := epay.NewClient(epayConfig.Pid, epayConfig.Url, epayConfig.Key, epayConfig.Type) // Convert order amount to CNY using current exchange rate amount, err := l.queryExchangeRate("CNY", info.Amount) @@ -275,16 +276,29 @@ func (l *PurchaseCheckoutLogic) epayPayment(config *payment.Payment, info *order return "", err } + // gateway mod + + isGatewayMod := report.IsGatewayMode() + // Build notification URL for payment status callbacks notifyUrl := "" if config.Domain != "" { - notifyUrl = config.Domain + "/v1/notify/" + config.Platform + "/" + config.Token + notifyUrl = config.Domain + if isGatewayMod { + notifyUrl += "/api/" + } + notifyUrl = notifyUrl + "/v1/notify/" + config.Platform + "/" + config.Token } else { host, ok := l.ctx.Value(constant.CtxKeyRequestHost).(string) if !ok { host = l.svcCtx.Config.Host } - notifyUrl = "https://" + host + "/v1/notify/" + config.Platform + "/" + config.Token + + notifyUrl = "https://" + host + if isGatewayMod { + notifyUrl += "/api" + } + notifyUrl = notifyUrl + "/v1/notify/" + config.Platform + "/" + config.Token } // Create payment URL for user redirection @@ -309,7 +323,7 @@ func (l *PurchaseCheckoutLogic) CryptoSaaSPayment(config *payment.Payment, info return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error()) } // Initialize EPay client with merchant credentials - client := epay.NewClient(epayConfig.AccountID, epayConfig.Endpoint, epayConfig.SecretKey) + client := epay.NewClient(epayConfig.AccountID, epayConfig.Endpoint, epayConfig.SecretKey, epayConfig.Type) // Convert order amount to CNY using current exchange rate amount, err := l.queryExchangeRate("CNY", info.Amount) @@ -317,18 +331,29 @@ func (l *PurchaseCheckoutLogic) CryptoSaaSPayment(config *payment.Payment, info return "", err } + // gateway mod + isGatewayMod := report.IsGatewayMode() + // Build notification URL for payment status callbacks notifyUrl := "" if config.Domain != "" { - notifyUrl = config.Domain + "/v1/notify/" + config.Platform + "/" + config.Token + notifyUrl = config.Domain + if isGatewayMod { + notifyUrl += "/api/" + } + notifyUrl = notifyUrl + "/v1/notify/" + config.Platform + "/" + config.Token } else { host, ok := l.ctx.Value(constant.CtxKeyRequestHost).(string) if !ok { host = l.svcCtx.Config.Host } - notifyUrl = "https://" + host + "/v1/notify/" + config.Platform + "/" + config.Token - } + notifyUrl = "https://" + host + if isGatewayMod { + notifyUrl += "/api" + } + notifyUrl = notifyUrl + "/v1/notify/" + config.Platform + "/" + config.Token + } // Create payment URL for user redirection url := client.CreatePayUrl(epay.Order{ Name: l.svcCtx.Config.Site.SiteName, @@ -347,6 +372,11 @@ func (l *PurchaseCheckoutLogic) queryExchangeRate(to string, src int64) (amount // Convert cents to decimal amount amount = float64(src) / float64(100) + if l.svcCtx.ExchangeRate != 0 && to == "CNY" { + amount = amount * l.svcCtx.ExchangeRate + return amount, nil + } + // Retrieve system currency configuration currency, err := l.svcCtx.SystemModel.GetCurrencyConfig(l.ctx) if err != nil { diff --git a/internal/logic/public/portal/purchaseLogic.go b/internal/logic/public/portal/purchaseLogic.go index 2c0cd69..322f94c 100644 --- a/internal/logic/public/portal/purchaseLogic.go +++ b/internal/logic/public/portal/purchaseLogic.go @@ -83,6 +83,12 @@ func (l *PurchaseLogic) Purchase(req *types.PortalPurchaseRequest) (resp *types. if couponInfo.Count != 0 && couponInfo.Count <= couponInfo.UsedCount { return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponInsufficientUsage), "coupon used") } + // Check expiration time + expireTime := time.Unix(couponInfo.ExpireTime, 0) + if time.Now().After(expireTime) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponExpired), "coupon expired") + } + couponSub := tool.StringToInt64Slice(couponInfo.Subscribe) if len(couponSub) > 0 && !tool.Contains(couponSub, req.SubscribeId) { return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotApplicable), "coupon not match") diff --git a/internal/logic/public/subscribe/querySubscribeListLogic.go b/internal/logic/public/subscribe/querySubscribeListLogic.go index 522d7c6..8c50115 100644 --- a/internal/logic/public/subscribe/querySubscribeListLogic.go +++ b/internal/logic/public/subscribe/querySubscribeListLogic.go @@ -59,8 +59,7 @@ func (l *QuerySubscribeListLogic) QuerySubscribeList(req *types.QuerySubscribeLi list[i] = sub // 通过服务组查询关联的节点数量 if item.ServerGroup != "" { - groupIds := tool.StringToInt64Slice(item.ServerGroup) - servers, err := l.svcCtx.ServerModel.FindServerListByGroupIds(l.ctx, groupIds) + servers, err := l.svcCtx.ServerModel.FindServerListByGroupIds(l.ctx, tool.StringToInt64Slice(item.ServerGroup)) if err != nil { l.Errorw("[QuerySubscribeListLogic] FindServerListByGroupIds error", logger.Field("error", err.Error())) sub.ServerCount = 0 diff --git a/internal/logic/public/subscribe/queryUserSubscribeNodeListLogic.go b/internal/logic/public/subscribe/queryUserSubscribeNodeListLogic.go new file mode 100644 index 0000000..2ad05b8 --- /dev/null +++ b/internal/logic/public/subscribe/queryUserSubscribeNodeListLogic.go @@ -0,0 +1,195 @@ +package subscribe + +import ( + "context" + "strings" + "time" + + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/constant" + "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 QueryUserSubscribeNodeListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get user subscribe node info +func NewQueryUserSubscribeNodeListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryUserSubscribeNodeListLogic { + return &QueryUserSubscribeNodeListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryUserSubscribeNodeListLogic) QueryUserSubscribeNodeList() (resp *types.QueryUserSubscribeNodeListResponse, err error) { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + + userSubscribes, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id, 1, 2) + if err != nil { + logger.Errorw("failed to query user subscribe", logger.Field("error", err.Error()), logger.Field("user_id", u.Id)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "DB_ERROR") + } + + resp = &types.QueryUserSubscribeNodeListResponse{} + for _, us := range userSubscribes { + userSubscribe, err := l.getUserSubscribe(us.Token) + if err != nil { + l.Errorw("[SubscribeLogic] Get user subscribe failed", logger.Field("error", err.Error()), logger.Field("token", userSubscribe.Token)) + return nil, err + } + nodes, err := l.getServers(userSubscribe) + if err != nil { + return nil, err + } + userSubscribeInfo := types.UserSubscribeInfo{ + Id: userSubscribe.Id, + Nodes: nodes, + Traffic: userSubscribe.Traffic, + Upload: userSubscribe.Upload, + Download: userSubscribe.Download, + Token: userSubscribe.Token, + UserId: userSubscribe.UserId, + OrderId: userSubscribe.OrderId, + SubscribeId: userSubscribe.SubscribeId, + StartTime: userSubscribe.StartTime.Unix(), + ExpireTime: userSubscribe.ExpireTime.Unix(), + Status: userSubscribe.Status, + CreatedAt: userSubscribe.CreatedAt.Unix(), + UpdatedAt: userSubscribe.UpdatedAt.Unix(), + } + + if userSubscribe.FinishedAt != nil { + userSubscribeInfo.FinishedAt = userSubscribe.FinishedAt.Unix() + } + + if l.svcCtx.Config.Register.EnableTrial && l.svcCtx.Config.Register.TrialSubscribe == userSubscribe.SubscribeId { + userSubscribeInfo.IsTryOut = true + } + + resp.List = append(resp.List, userSubscribeInfo) + } + + return +} + +func (l *QueryUserSubscribeNodeListLogic) getServers(userSub *user.Subscribe) (userSubscribeNodes []*types.UserSubscribeNodeInfo, err error) { + userSubscribeNodes = make([]*types.UserSubscribeNodeInfo, 0) + if l.isSubscriptionExpired(userSub) { + return l.createExpiredServers(), nil + } + + subDetails, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, userSub.SubscribeId) + if err != nil { + l.Errorw("[Generate Subscribe]find subscribe details error: %v", logger.Field("error", err.Error())) + 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, ",") + + l.Debugf("[Generate Subscribe]nodes: %v, NodeTags: %v", nodeIds, tags) + + enable := true + + _, nodes, err := l.svcCtx.NodeModel.FilterNodeList(l.ctx, &node.FilterNodeParams{ + Page: 0, + Size: 1000, + NodeId: nodeIds, + Enabled: &enable, // Only get enabled nodes + }) + + if len(nodes) > 0 { + var serverMapIds = make(map[int64]*node.Server) + for _, n := range nodes { + serverMapIds[n.ServerId] = nil + } + var serverIds []int64 + for k := range serverMapIds { + serverIds = append(serverIds, k) + } + + servers, err := l.svcCtx.NodeModel.QueryServerList(l.ctx, serverIds) + if err != nil { + l.Errorw("[Generate Subscribe]find server details error: %v", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find server details error: %v", err.Error()) + } + + for _, s := range servers { + serverMapIds[s.Id] = s + } + + for _, n := range nodes { + server := serverMapIds[n.ServerId] + if server == nil { + continue + } + userSubscribeNode := &types.UserSubscribeNodeInfo{ + Id: n.Id, + Name: n.Name, + Uuid: userSub.UUID, + Protocol: n.Protocol, + Port: n.Port, + Address: n.Address, + Tags: strings.Split(n.Tags, ","), + Country: server.Country, + City: server.City, + CreatedAt: n.CreatedAt.Unix(), + } + userSubscribeNodes = append(userSubscribeNodes, userSubscribeNode) + } + } + + l.Debugf("[Query Subscribe]found servers: %v", len(nodes)) + + if err != nil { + l.Errorw("[Generate Subscribe]find server details error: %v", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find server details error: %v", err.Error()) + } + logger.Debugf("[Generate Subscribe]found servers: %v", len(nodes)) + return userSubscribeNodes, nil +} + +func (l *QueryUserSubscribeNodeListLogic) isSubscriptionExpired(userSub *user.Subscribe) bool { + return userSub.ExpireTime.Unix() < time.Now().Unix() && userSub.ExpireTime.Unix() != 0 +} + +func (l *QueryUserSubscribeNodeListLogic) createExpiredServers() []*types.UserSubscribeNodeInfo { + return nil +} + +func (l *QueryUserSubscribeNodeListLogic) getFirstHostLine() string { + host := l.svcCtx.Config.Host + lines := strings.Split(host, "\n") + if len(lines) > 0 { + return lines[0] + } + return host +} +func (l *QueryUserSubscribeNodeListLogic) getUserSubscribe(token string) (*user.Subscribe, error) { + userSub, err := l.svcCtx.UserModel.FindOneSubscribeByToken(l.ctx, token) + if err != nil { + l.Infow("[Generate Subscribe]find subscribe error: %v", logger.Field("error", err.Error()), logger.Field("token", token)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error()) + } + + // Ignore expiration check + //if userSub.Status > 1 { + // l.Infow("[Generate Subscribe]subscribe is not available", logger.Field("status", int(userSub.Status)), logger.Field("token", token)) + // return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeNotAvailable), "subscribe is not available") + //} + + return userSub, nil +} diff --git a/internal/logic/public/user/commissionWithdrawLogic.go b/internal/logic/public/user/commissionWithdrawLogic.go new file mode 100644 index 0000000..d16dec0 --- /dev/null +++ b/internal/logic/public/user/commissionWithdrawLogic.go @@ -0,0 +1,108 @@ +package user + +import ( + "context" + "time" + + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type CommissionWithdrawLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Commission Withdraw +func NewCommissionWithdrawLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CommissionWithdrawLogic { + return &CommissionWithdrawLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CommissionWithdrawLogic) CommissionWithdraw(req *types.CommissionWithdrawRequest) (resp *types.WithdrawalLog, err error) { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + + if u.Commission < req.Amount { + logger.Errorf("User %d has insufficient commission balance: %.2f, requested: %.2f", u.Id, float64(u.Commission)/100, float64(req.Amount)/100) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserCommissionNotEnough), "User %d has insufficient commission balance", u.Id) + } + + tx := l.svcCtx.DB.WithContext(l.ctx).Begin() + + // update user commission balance + u.Commission -= req.Amount + if err = l.svcCtx.UserModel.Update(l.ctx, u, tx); err != nil { + tx.Rollback() + l.Errorf("Failed to update user %d commission balance: %v", u.Id, err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "Failed to update user %d commission balance: %v", u.Id, err) + } + + // create withdrawal log + logInfo := log.Commission{ + Type: log.CommissionTypeConvertBalance, + Amount: req.Amount, + Timestamp: time.Now().UnixMilli(), + } + b, err := logInfo.Marshal() + + if err != nil { + tx.Rollback() + l.Errorf("Failed to marshal commission log for user %d: %v", u.Id, err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Failed to marshal commission log for user %d: %v", u.Id, err) + } + + err = tx.Model(log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeCommission.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: u.Id, + Content: string(b), + CreatedAt: time.Now(), + }).Error + + if err != nil { + tx.Rollback() + l.Errorf("Failed to create commission log for user %d: %v", u.Id, err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "Failed to create commission log for user %d: %v", u.Id, err) + } + + err = tx.Model(&user.Withdrawal{}).Create(&user.Withdrawal{ + UserId: u.Id, + Amount: req.Amount, + Content: req.Content, + Status: 0, + Reason: "", + }).Error + + if err != nil { + tx.Rollback() + l.Errorf("Failed to create withdrawal log for user %d: %v", u.Id, err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "Failed to create withdrawal log for user %d: %v", u.Id, err) + } + if err = tx.Commit().Error; err != nil { + l.Errorf("Transaction commit failed for user %d withdrawal: %v", u.Id, err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Transaction commit failed for user %d withdrawal: %v", u.Id, err) + } + + return &types.WithdrawalLog{ + UserId: u.Id, + Amount: req.Amount, + Content: req.Content, + Status: 0, + Reason: "", + CreatedAt: time.Now().UnixMilli(), + }, nil +} diff --git a/internal/logic/public/user/getDeviceListLogic.go b/internal/logic/public/user/getDeviceListLogic.go new file mode 100644 index 0000000..76722d5 --- /dev/null +++ b/internal/logic/public/user/getDeviceListLogic.go @@ -0,0 +1,39 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" +) + +type GetDeviceListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get Device List +func NewGetDeviceListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetDeviceListLogic { + return &GetDeviceListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetDeviceListLogic) GetDeviceList() (resp *types.GetDeviceListResponse, err error) { + userInfo := l.ctx.Value(constant.CtxKeyUser).(*user.User) + list, count, err := l.svcCtx.UserModel.QueryDeviceList(l.ctx, userInfo.Id) + userRespList := make([]types.UserDevice, 0) + tool.DeepCopy(&userRespList, list) + resp = &types.GetDeviceListResponse{ + Total: count, + List: userRespList, + } + return +} diff --git a/internal/logic/public/user/queryUserSubscribeLogic.go b/internal/logic/public/user/queryUserSubscribeLogic.go index aa55391..55e3770 100644 --- a/internal/logic/public/user/queryUserSubscribeLogic.go +++ b/internal/logic/public/user/queryUserSubscribeLogic.go @@ -60,25 +60,8 @@ func (l *QueryUserSubscribeLogic) QueryUserSubscribe() (resp *types.QueryUserSub } } - // 计算节点数量(通过服务组关联的实际节点数量) - if item.Subscribe != nil { - // 获取服务组ID列表 - groupIds := tool.StringToInt64Slice(item.Subscribe.ServerGroup) - - // 通过服务组查询关联的节点数量 - servers, err := l.svcCtx.ServerModel.FindServerListByGroupIds(l.ctx, groupIds) - if err != nil { - l.Errorw("[QueryUserSubscribeLogic] FindServerListByGroupIds error", logger.Field("error", err.Error())) - sub.Subscribe.ServerCount = 0 - } else { - sub.Subscribe.ServerCount = int64(len(servers)) - } - - // 保留原始服务器ID列表用于其他用途 - serverIds := tool.StringToInt64Slice(item.Subscribe.Server) - sub.Subscribe.Server = serverIds - } - + short, _ := tool.FixedUniqueString(item.Token, 8, "") + sub.Short = short sub.ResetTime = calculateNextResetTime(&sub) resp.List = append(resp.List, sub) } diff --git a/internal/logic/public/user/queryWithdrawalLogLogic.go b/internal/logic/public/user/queryWithdrawalLogLogic.go new file mode 100644 index 0000000..1b1a583 --- /dev/null +++ b/internal/logic/public/user/queryWithdrawalLogLogic.go @@ -0,0 +1,30 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" +) + +type QueryWithdrawalLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewQueryWithdrawalLogLogic Query Withdrawal Log +func NewQueryWithdrawalLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryWithdrawalLogLogic { + return &QueryWithdrawalLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryWithdrawalLogLogic) QueryWithdrawalLog(req *types.QueryWithdrawalLogListRequest) (resp *types.QueryWithdrawalLogListResponse, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/internal/logic/public/user/unbindDeviceLogic.go b/internal/logic/public/user/unbindDeviceLogic.go new file mode 100644 index 0000000..57218cc --- /dev/null +++ b/internal/logic/public/user/unbindDeviceLogic.go @@ -0,0 +1,72 @@ +package user + +import ( + "context" + "fmt" + + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type UnbindDeviceLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Unbind Device +func NewUnbindDeviceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UnbindDeviceLogic { + return &UnbindDeviceLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UnbindDeviceLogic) UnbindDevice(req *types.UnbindDeviceRequest) error { + userInfo := l.ctx.Value(constant.CtxKeyUser).(*user.User) + device, err := l.svcCtx.UserModel.FindOneDevice(l.ctx, req.Id) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DeviceNotExist), "find device") + } + + if device.UserId != userInfo.Id { + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "device not belong to user") + } + + return l.svcCtx.DB.Transaction(func(tx *gorm.DB) error { + var deleteDevice user.Device + err = tx.Model(&deleteDevice).Where("id = ?", req.Id).First(&deleteDevice).Error + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.QueueEnqueueError), "find device err: %v", err) + } + err = tx.Delete(deleteDevice).Error + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete device err: %v", err) + } + var userAuth user.AuthMethods + err = tx.Model(&userAuth).Where("auth_identifier = ? and auth_type = ?", deleteDevice.Identifier, "device").First(&userAuth).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find device online record err: %v", err) + } + + err = tx.Delete(&userAuth).Error + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete device online record err: %v", err) + } + sessionId := l.ctx.Value(constant.CtxKeySessionID) + sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) + l.svcCtx.Redis.Del(l.ctx, sessionIdCacheKey) + return nil + }) +} diff --git a/internal/logic/public/user/updateUserRulesLogic.go b/internal/logic/public/user/updateUserRulesLogic.go new file mode 100644 index 0000000..63ab169 --- /dev/null +++ b/internal/logic/public/user/updateUserRulesLogic.go @@ -0,0 +1,51 @@ +package user + +import ( + "context" + "encoding/json" + + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type UpdateUserRulesLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewUpdateUserRulesLogic Update User Rules +func NewUpdateUserRulesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateUserRulesLogic { + return &UpdateUserRulesLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateUserRulesLogic) UpdateUserRules(req *types.UpdateUserRulesRequest) error { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + if len(req.Rules) > 0 { + bytes, err := json.Marshal(req.Rules) + if err != nil { + l.Logger.Errorf("UpdateUserRulesLogic json marshal rules error: %v", err) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "json marshal rules failed: %v", err.Error()) + } + u.Rules = string(bytes) + err = l.svcCtx.UserModel.Update(l.ctx, u) + if err != nil { + l.Logger.Errorf("UpdateUserRulesLogic UpdateUserRules error: %v", err) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update user rules failed: %v", err.Error()) + } + } + return nil +} diff --git a/internal/logic/public/user/updateUserSubscribeNoteLogic.go b/internal/logic/public/user/updateUserSubscribeNoteLogic.go new file mode 100644 index 0000000..3c43a8d --- /dev/null +++ b/internal/logic/public/user/updateUserSubscribeNoteLogic.go @@ -0,0 +1,73 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/server/pkg/constant" + + "github.com/perfect-panel/server/internal/model/user" + "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 UpdateUserSubscribeNoteLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewUpdateUserSubscribeNoteLogic Update User Subscribe Note +func NewUpdateUserSubscribeNoteLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateUserSubscribeNoteLogic { + return &UpdateUserSubscribeNoteLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateUserSubscribeNoteLogic) UpdateUserSubscribeNote(req *types.UpdateUserSubscribeNoteRequest) error { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + logger.Error("current user is not found in context") + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + + userSub, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.UserSubscribeId) + if err != nil { + l.Errorw("FindOneUserSubscribe failed:", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOneUserSubscribe failed: %v", err.Error()) + } + + if userSub.UserId != u.Id { + l.Errorw("UserSubscribeId does not belong to the current user") + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "UserSubscribeId does not belong to the current user") + } + + userSub.Note = req.Note + var newSub user.Subscribe + tool.DeepCopy(&newSub, userSub) + + err = l.svcCtx.UserModel.UpdateSubscribe(l.ctx, &newSub) + if err != nil { + l.Errorw("UpdateSubscribe failed:", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "UpdateSubscribe failed: %v", err.Error()) + } + + // Clear user subscription cache + if err = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, &newSub); err != nil { + l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("userSubscribeId", userSub.Id)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "ClearSubscribeCache failed: %v", err.Error()) + } + + // Clear subscription cache + if err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, userSub.SubscribeId); err != nil { + l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("subscribeId", userSub.SubscribeId)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "ClearSubscribeCache failed: %v", err.Error()) + } + + return nil +} diff --git a/internal/logic/server/constant.go b/internal/logic/server/constant.go index 7515e9e..e2d1584 100644 --- a/internal/logic/server/constant.go +++ b/internal/logic/server/constant.go @@ -9,7 +9,9 @@ const ( AnyTLS = "anytls" Tuic = "tuic" Hysteria = "hysteria" - Hysteria2 = "hysteria2" + // Deprecated: Hysteria2 is deprecated, use Hysteria instead + // TODO: remove in future versions + Hysteria2 = "hysteria2" ) type SecurityConfig struct { diff --git a/internal/logic/server/getServerConfigLogic.go b/internal/logic/server/getServerConfigLogic.go index b3e3240..94221a9 100644 --- a/internal/logic/server/getServerConfigLogic.go +++ b/internal/logic/server/getServerConfigLogic.go @@ -57,13 +57,19 @@ func (l *GetServerConfigLogic) GetServerConfig(req *types.GetServerConfigRequest return nil, err } + // compatible hysteria2, remove in future versions + protocolRequest := req.Protocol + if protocolRequest == Hysteria2 { + protocolRequest = Hysteria + } + protocols, err := data.UnmarshalProtocols() if err != nil { return nil, err } var cfg map[string]interface{} for _, protocol := range protocols { - if protocol.Type == req.Protocol { + if protocol.Type == protocolRequest { cfg = l.compatible(protocol) break } @@ -209,7 +215,7 @@ func (l *GetServerConfigLogic) compatible(config node.Protocol) map[string]inter RealityShortId: config.RealityShortId, }, } - case Hysteria2, Hysteria: + case Hysteria: result = Hysteria2Node{ Port: config.Port, HopPorts: config.HopPorts, diff --git a/internal/logic/subscribe/subscribeLogic.go b/internal/logic/subscribe/subscribeLogic.go index e3dfd73..6a4a6e5 100644 --- a/internal/logic/subscribe/subscribeLogic.go +++ b/internal/logic/subscribe/subscribeLogic.go @@ -249,8 +249,9 @@ func (l *SubscribeLogic) createExpiredServers() []*node.Node { Port: 18080, Address: "127.0.0.1", Server: &node.Server{ + Id: 1, Name: "Subscribe Expired", - Protocols: "[{\"type:\"\"shadowsocks\",\"cipher\":\"aes-256-gcm\",\"port\":1}]", + Protocols: "[{\"type\":\"shadowsocks\",\"cipher\":\"aes-256-gcm\",\"port\":1}]", }, Protocol: "shadowsocks", Enabled: &enable, @@ -261,8 +262,9 @@ func (l *SubscribeLogic) createExpiredServers() []*node.Node { Port: 18080, Address: "127.0.0.1", Server: &node.Server{ + Id: 1, Name: "Subscribe Expired", - Protocols: "[{\"type:\"\"shadowsocks\",\"cipher\":\"aes-256-gcm\",\"port\":1}]", + Protocols: "[{\"type\":\"shadowsocks\",\"cipher\":\"aes-256-gcm\",\"port\":1}]", }, Protocol: "shadowsocks", Enabled: &enable, diff --git a/internal/middleware/authMiddleware.go b/internal/middleware/authMiddleware.go index a76d0ee..fbf3758 100644 --- a/internal/middleware/authMiddleware.go +++ b/internal/middleware/authMiddleware.go @@ -40,6 +40,11 @@ func AuthMiddleware(svc *svc.ServiceContext) func(c *gin.Context) { c.Abort() return } + + loginType := "" + if claims["LoginType"] != nil { + loginType = claims["LoginType"].(string) + } // get user id from token userId := int64(claims["UserId"].(float64)) // get session id from token @@ -77,6 +82,7 @@ func AuthMiddleware(svc *svc.ServiceContext) func(c *gin.Context) { c.Abort() return } + ctx = context.WithValue(ctx, constant.LoginType, loginType) ctx = context.WithValue(ctx, constant.CtxKeyUser, userInfo) ctx = context.WithValue(ctx, constant.CtxKeySessionID, sessionId) c.Request = c.Request.WithContext(ctx) diff --git a/internal/middleware/deviceMiddleware.go b/internal/middleware/deviceMiddleware.go new file mode 100644 index 0000000..b66ccb0 --- /dev/null +++ b/internal/middleware/deviceMiddleware.go @@ -0,0 +1,295 @@ +package middleware + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "strings" + + "github.com/perfect-panel/server/internal/svc" + pkgaes "github.com/perfect-panel/server/pkg/aes" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/result" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" + + "github.com/gin-gonic/gin" +) + +const ( + noWritten = -1 + defaultStatus = http.StatusOK +) + +func DeviceMiddleware(srvCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + if !srvCtx.Config.Device.Enable { + c.Next() + return + } + + if srvCtx.Config.Device.SecuritySecret == "" { + result.HttpResult(c, nil, errors.Wrapf(xerr.NewErrCode(xerr.SecretIsEmpty), "Secret is empty")) + c.Abort() + return + } + + ctx := c.Request.Context() + if ctx.Value(constant.CtxKeyUser) == nil && c.GetHeader("Login-Type") != "" { + ctx = context.WithValue(ctx, constant.LoginType, c.GetHeader("Login-Type")) + c.Request = c.Request.WithContext(ctx) + } + + loginType, ok := ctx.Value(constant.LoginType).(string) + if !ok || loginType != "device" { + c.Next() + return + } + + rw := NewResponseWriter(c, srvCtx) + if !rw.Decrypt() { + result.HttpResult(c, nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidCiphertext), "Invalid ciphertext")) + c.Abort() + return + } + c.Writer = rw + c.Next() + rw.FlushAbort() + } +} + +func NewResponseWriter(c *gin.Context, srvCtx *svc.ServiceContext) (rw *ResponseWriter) { + rw = &ResponseWriter{ + c: c, + body: new(bytes.Buffer), + ResponseWriter: c.Writer, + } + rw.encryptionKey = srvCtx.Config.Device.SecuritySecret + rw.encryptionMethod = "AES" + rw.encryption = true + return rw +} + +func (rw *ResponseWriter) Encrypt() { + if !rw.encryption { + return + } + buf := rw.body.Bytes() + params := map[string]interface{}{} + err := json.Unmarshal(buf, ¶ms) + if err != nil { + return + } + data := params["data"] + if data != nil { + var jsonData []byte + str, ok := data.(string) + if ok { + jsonData = []byte(str) + } else { + jsonData, _ = json.Marshal(data) + } + encrypt, iv, err := pkgaes.Encrypt(jsonData, rw.encryptionKey) + if err != nil { + return + } + params["data"] = map[string]interface{}{ + "data": encrypt, + "time": iv, + } + + } + marshal, _ := json.Marshal(params) + rw.body.Reset() + rw.body.Write(marshal) +} + +func (rw *ResponseWriter) Decrypt() bool { + if !rw.encryption { + return true + } + + //判断url链接中是否存在data和iv数据,存在就进行解密并设置回去 + query := rw.c.Request.URL.Query() + dataStr := query.Get("data") + timeStr := query.Get("time") + if dataStr != "" && timeStr != "" { + decrypt, err := pkgaes.Decrypt(dataStr, rw.encryptionKey, timeStr) + if err == nil { + params := map[string]interface{}{} + err = json.Unmarshal([]byte(decrypt), ¶ms) + if err == nil { + for k, v := range params { + query.Set(k, fmt.Sprintf("%v", v)) + } + query.Del("data") + query.Del("time") + rw.c.Request.RequestURI = fmt.Sprintf("%s?%s", rw.c.Request.RequestURI[:strings.Index(rw.c.Request.RequestURI, "?")], query.Encode()) + rw.c.Request.URL.RawQuery = query.Encode() + } + } + } + + //判断body是否存在数据,存在就尝试解密,并设置回去 + body, err := io.ReadAll(rw.c.Request.Body) + if err != nil { + return true + } + + if len(body) == 0 { + return true + } + + params := map[string]interface{}{} + err = json.Unmarshal(body, ¶ms) + data := params["data"] + nonce := params["time"] + if err != nil || data == nil { + return false + } + + str, ok := data.(string) + if !ok { + return false + } + iv, ok := nonce.(string) + if !ok { + return false + } + + decrypt, err := pkgaes.Decrypt(str, rw.encryptionKey, iv) + if err != nil { + return false + } + rw.c.Request.Body = io.NopCloser(bytes.NewBuffer([]byte(decrypt))) + return true +} + +func (rw *ResponseWriter) FlushAbort() { + defer rw.c.Abort() + responseBody := rw.body.String() + fmt.Println("Original Response Body:", responseBody) + rw.flush = true + if rw.encryption { + rw.Encrypt() + } + _, err := rw.Write(rw.body.Bytes()) + if err != nil { + return + } +} + +type ResponseWriter struct { + http.ResponseWriter + size int + status int + flush bool + body *bytes.Buffer + c *gin.Context + encryption bool + encryptionKey string + encryptionMethod string +} + +func (rw *ResponseWriter) Unwrap() http.ResponseWriter { + return rw.ResponseWriter +} + +//nolint:unused +func (rw *ResponseWriter) reset(writer http.ResponseWriter) { + rw.ResponseWriter = writer + rw.size = noWritten + rw.status = defaultStatus +} + +func (rw *ResponseWriter) WriteHeader(code int) { + if code > 0 && rw.status != code { + if rw.Written() { + return + } + rw.status = code + } +} + +func (rw *ResponseWriter) WriteHeaderNow() { + if !rw.Written() { + rw.size = 0 + rw.ResponseWriter.WriteHeader(rw.status) + } +} + +func (rw *ResponseWriter) Write(data []byte) (n int, err error) { + if rw.flush { + rw.WriteHeaderNow() + n, err = rw.ResponseWriter.Write(data) + rw.size += n + } else { + rw.body.Write(data) + } + return +} + +func (rw *ResponseWriter) WriteString(s string) (n int, err error) { + if rw.flush { + rw.WriteHeaderNow() + n, err = rw.ResponseWriter.Write([]byte(s)) + rw.size += n + } else { + rw.body.Write([]byte(s)) + } + return +} + +func (rw *ResponseWriter) Status() int { + return rw.status +} + +func (rw *ResponseWriter) Size() int { + return rw.size +} + +func (rw *ResponseWriter) Written() bool { + return rw.size != noWritten +} + +// Hijack implements the http.Hijacker interface. +func (rw *ResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + if rw.size < 0 { + rw.size = 0 + } + return rw.ResponseWriter.(http.Hijacker).Hijack() +} + +// CloseNotify implements the http.CloseNotifier interface. +func (rw *ResponseWriter) CloseNotify() <-chan bool { + // 通过 r.Context().Done() 来监听请求的取消 + done := rw.c.Request.Context().Done() + closed := make(chan bool) + + // 当上下文被取消时,通过 closed channel 发送通知 + go func() { + <-done + closed <- true + }() + + return closed +} + +// Flush implements the http.Flusher interface. +func (rw *ResponseWriter) Flush() { + rw.WriteHeaderNow() + rw.ResponseWriter.(http.Flusher).Flush() +} + +func (rw *ResponseWriter) Pusher() (pusher http.Pusher) { + if pusher, ok := rw.ResponseWriter.(http.Pusher); ok { + return pusher + } + return nil +} diff --git a/internal/model/announcement/model.go b/internal/model/announcement/model.go index f5575a8..973fc97 100644 --- a/internal/model/announcement/model.go +++ b/internal/model/announcement/model.go @@ -43,7 +43,7 @@ func (m *customAnnouncementModel) GetAnnouncementListByPage(ctx context.Context, if filter.Search != "" { conn = conn.Where("`title` LIKE ? OR `content` LIKE ?", "%"+filter.Search+"%", "%"+filter.Search+"%") } - return conn.Count(&total).Offset((page - 1) * size).Limit(size).Find(v).Error + return conn.Count(&total).Offset((page - 1) * size).Limit(size).Find(&list).Error }) return total, list, err } diff --git a/internal/model/log/log.go b/internal/model/log/log.go index af3eb98..1afd13d 100644 --- a/internal/model/log/log.go +++ b/internal/model/log/log.go @@ -33,23 +33,24 @@ const ( TypeTrafficStat Type = 42 // Daily traffic statistics log ) const ( - ResetSubscribeTypeAuto uint16 = 231 // Auto reset - ResetSubscribeTypeAdvance uint16 = 232 // Advance reset - ResetSubscribeTypePaid uint16 = 233 // Paid reset - ResetSubscribeTypeQuota uint16 = 234 // Quota reset - BalanceTypeRecharge uint16 = 321 // Recharge - BalanceTypeWithdraw uint16 = 322 // Withdraw - BalanceTypePayment uint16 = 323 // Payment - BalanceTypeRefund uint16 = 324 // Refund - BalanceTypeAdjust uint16 = 326 // Admin Adjust - BalanceTypeReward uint16 = 325 // Reward - CommissionTypePurchase uint16 = 331 // Purchase - CommissionTypeRenewal uint16 = 332 // Renewal - CommissionTypeRefund uint16 = 333 // Refund - commissionTypeWithdraw uint16 = 334 // withdraw - CommissionTypeAdjust uint16 = 335 // Admin Adjust - GiftTypeIncrease uint16 = 341 // Increase - GiftTypeReduce uint16 = 342 // Reduce + ResetSubscribeTypeAuto uint16 = 231 // Auto reset + ResetSubscribeTypeAdvance uint16 = 232 // Advance reset + ResetSubscribeTypePaid uint16 = 233 // Paid reset + ResetSubscribeTypeQuota uint16 = 234 // Quota reset + BalanceTypeRecharge uint16 = 321 // Recharge + BalanceTypeWithdraw uint16 = 322 // Withdraw + BalanceTypePayment uint16 = 323 // Payment + BalanceTypeRefund uint16 = 324 // Refund + BalanceTypeAdjust uint16 = 326 // Admin Adjust + BalanceTypeReward uint16 = 325 // Reward + CommissionTypePurchase uint16 = 331 // Purchase + CommissionTypeRenewal uint16 = 332 // Renewal + CommissionTypeRefund uint16 = 333 // Refund + CommissionTypeWithdraw uint16 = 334 // withdraw + CommissionTypeAdjust uint16 = 335 // Admin Adjust + CommissionTypeConvertBalance uint16 = 336 // Convert to Balance + GiftTypeIncrease uint16 = 341 // Increase + GiftTypeReduce uint16 = 342 // Reduce ) // Uint8 converts Type to uint8. diff --git a/internal/model/node/default.go b/internal/model/node/default.go index 575ea5f..ebdf331 100644 --- a/internal/model/node/default.go +++ b/internal/model/node/default.go @@ -23,6 +23,7 @@ type ( UpdateServer(ctx context.Context, data *Server, tx ...*gorm.DB) error DeleteServer(ctx context.Context, id int64, tx ...*gorm.DB) error Transaction(ctx context.Context, fn func(db *gorm.DB) error) error + QueryServerList(ctx context.Context, ids []int64) (servers []*Server, err error) } NodeModel interface { diff --git a/internal/model/node/model.go b/internal/model/node/model.go index 8828a99..ddfa736 100644 --- a/internal/model/node/model.go +++ b/internal/model/node/model.go @@ -3,8 +3,10 @@ package node import ( "context" "fmt" + "strings" "github.com/perfect-panel/server/pkg/tool" + "gorm.io/gorm" ) type customServerLogicModel interface { @@ -63,6 +65,12 @@ func (m *customServerModel) FilterServerList(ctx context.Context, params *Filter return total, servers, err } +func (m *customServerModel) QueryServerList(ctx context.Context, ids []int64) (servers []*Server, err error) { + query := m.WithContext(ctx).Model(&Server{}) + err = query.Where("id IN (?)", ids).Find(&servers).Error + return +} + // FilterNodeList Filter Node List func (m *customServerModel) FilterNodeList(ctx context.Context, params *FilterNodeParams) (int64, []*Node, error) { var nodes []*Node @@ -85,10 +93,7 @@ func (m *customServerModel) FilterNodeList(ctx context.Context, params *FilterNo query = query.Where("server_id IN ?", params.ServerId) } if len(params.Tag) > 0 { - query = query.Where("1 = 0") - for _, tag := range params.Tag { - query = query.Or("FIND_IN_SET(?,tags)", tag) - } + query = query.Scopes(InSet("tags", params.Tag)) } if params.Protocol != "" { query = query.Where("protocol = ?", params.Protocol) @@ -165,3 +170,22 @@ func (m *customServerModel) ClearServerCache(ctx context.Context, serverId int64 } return nil } + +// InSet 支持多值 OR 查询 +func InSet(field string, values []string) func(db *gorm.DB) *gorm.DB { + return func(db *gorm.DB) *gorm.DB { + if len(values) == 0 { + return db + } + + conds := make([]string, len(values)) + args := make([]interface{}, len(values)) + for i, v := range values { + conds[i] = "FIND_IN_SET(?, " + field + ")" + args[i] = v + } + + // 用括号包裹 OR 条件,保证外层 AND 不受影响 + return db.Where("("+strings.Join(conds, " OR ")+")", args...) + } +} diff --git a/internal/model/payment/payment.go b/internal/model/payment/payment.go index 46ee0a0..ad2f046 100644 --- a/internal/model/payment/payment.go +++ b/internal/model/payment/payment.go @@ -85,9 +85,10 @@ func (l *AlipayF2FConfig) Unmarshal(data []byte) error { } type EPayConfig struct { - Pid string `json:"pid"` - Url string `json:"url"` - Key string `json:"key"` + Pid string `json:"pid"` + Url string `json:"url"` + Key string `json:"key"` + Type string `json:"type"` } func (l *EPayConfig) Marshal() ([]byte, error) { @@ -109,6 +110,7 @@ type CryptoSaaSConfig struct { Endpoint string `json:"endpoint"` AccountID string `json:"account_id"` SecretKey string `json:"secret_key"` + Type string `json:"type"` } func (l *CryptoSaaSConfig) Marshal() ([]byte, error) { diff --git a/internal/model/subscribe/subscribe.go b/internal/model/subscribe/subscribe.go index 456f0a5..1a44121 100644 --- a/internal/model/subscribe/subscribe.go +++ b/internal/model/subscribe/subscribe.go @@ -22,8 +22,7 @@ type Subscribe struct { Quota int64 `gorm:"type:int;not null;default:0;comment:Quota"` Nodes string `gorm:"type:varchar(255);comment:Node Ids"` NodeTags string `gorm:"type:varchar(255);comment:Node Tags"` - ServerGroup string `gorm:"type:varchar(255);comment:Server Group Ids"` - Server string `gorm:"type:varchar(255);comment:Server Ids"` + ServerGroup string `gorm:"type:varchar(255);comment:Server Group"` Show *bool `gorm:"type:tinyint(1);not null;default:0;comment:Show portal page"` Sell *bool `gorm:"type:tinyint(1);not null;default:0;comment:Sell"` Sort int64 `gorm:"type:int;not null;default:0;comment:Sort"` diff --git a/internal/model/user/device.go b/internal/model/user/device.go index ed57df7..b1194a7 100644 --- a/internal/model/user/device.go +++ b/internal/model/user/device.go @@ -46,6 +46,16 @@ func (m *customUserModel) QueryDevicePageList(ctx context.Context, userId, subsc return list, total, err } +// QueryDeviceList returns a list of records that meet the conditions. +func (m *customUserModel) QueryDeviceList(ctx context.Context, userId int64) ([]*Device, int64, error) { + var list []*Device + var total int64 + err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Device{}).Where("`user_id` = ?", userId).Count(&total).Find(&list).Error + }) + return list, total, err +} + func (m *customUserModel) UpdateDevice(ctx context.Context, data *Device, tx ...*gorm.DB) error { old, err := m.FindOneDevice(ctx, data.Id) if err != nil { @@ -76,3 +86,18 @@ func (m *customUserModel) DeleteDevice(ctx context.Context, id int64, tx ...*gor }, data.GetCacheKeys()...) return err } + +func (m *customUserModel) InsertDevice(ctx context.Context, data *Device, tx ...*gorm.DB) error { + defer func() { + if clearErr := m.ClearDeviceCache(ctx, data); clearErr != nil { + // log cache clear error + } + }() + + return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Create(data).Error + }) +} diff --git a/internal/model/user/model.go b/internal/model/user/model.go index 3c5dff9..86caa0d 100644 --- a/internal/model/user/model.go +++ b/internal/model/user/model.go @@ -36,6 +36,7 @@ type SubscribeDetails struct { Token string `gorm:"index:idx_token;unique;type:varchar(255);default:'';comment:Token"` UUID string `gorm:"type:varchar(255);unique;index:idx_uuid;default:'';comment:UUID"` Status uint8 `gorm:"type:tinyint(1);default:0;comment:Subscription Status: 0: Pending 1: Active 2: Finished 3: Expired; 4: Cancelled"` + Note string `gorm:"type:varchar(500);default:'';comment:User note for subscription"` CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` UpdatedAt time.Time `gorm:"comment:Update Time"` } @@ -95,10 +96,12 @@ type customUserLogicModel interface { FindUserAuthMethodByPlatform(ctx context.Context, userId int64, platform string) (*AuthMethods, error) FindOneByEmail(ctx context.Context, email string) (*User, error) FindOneDevice(ctx context.Context, id int64) (*Device, error) + QueryDeviceList(ctx context.Context, userid int64) ([]*Device, int64, error) QueryDevicePageList(ctx context.Context, userid, subscribeId int64, page, size int) ([]*Device, int64, error) UpdateDevice(ctx context.Context, data *Device, tx ...*gorm.DB) error FindOneDeviceByIdentifier(ctx context.Context, id string) (*Device, error) DeleteDevice(ctx context.Context, id int64, tx ...*gorm.DB) error + InsertDevice(ctx context.Context, data *Device, tx ...*gorm.DB) error ClearSubscribeCache(ctx context.Context, data ...*Subscribe) error ClearUserCache(ctx context.Context, data ...*User) error diff --git a/internal/model/user/user.go b/internal/model/user/user.go index 31e7096..2f9c88c 100644 --- a/internal/model/user/user.go +++ b/internal/model/user/user.go @@ -7,6 +7,8 @@ import ( type User struct { Id int64 `gorm:"primaryKey"` Password string `gorm:"type:varchar(100);not null;comment:User Password"` + Algo string `gorm:"type:varchar(20);default:'default';comment:Encryption Algorithm"` + Salt string `gorm:"type:varchar(20);default:null;comment:Password Salt"` Avatar string `gorm:"type:MEDIUMTEXT;comment:User Avatar"` Balance int64 `gorm:"default:0;comment:User Balance"` // User Balance Amount ReferCode string `gorm:"type:varchar(20);default:'';comment:Referral Code"` @@ -24,6 +26,7 @@ type User struct { EnableTradeNotify *bool `gorm:"default:false;not null;comment:Enable Trade Notifications"` AuthMethods []AuthMethods `gorm:"foreignKey:UserId;references:Id"` UserDevices []Device `gorm:"foreignKey:UserId;references:Id"` + Rules string `gorm:"type:TEXT;comment:User Rules"` CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` UpdatedAt time.Time `gorm:"comment:Update Time"` } @@ -47,6 +50,7 @@ type Subscribe struct { Token string `gorm:"index:idx_token;unique;type:varchar(255);default:'';comment:Token"` UUID string `gorm:"type:varchar(255);unique;index:idx_uuid;default:'';comment:UUID"` Status uint8 `gorm:"type:tinyint(1);default:0;comment:Subscription Status: 0: Pending 1: Active 2: Finished 3: Expired 4: Deducted"` + Note string `gorm:"type:varchar(500);default:'';comment:User note for subscription"` CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` UpdatedAt time.Time `gorm:"comment:Update Time"` } @@ -99,3 +103,18 @@ type DeviceOnlineRecord struct { func (DeviceOnlineRecord) TableName() string { return "user_device_online_record" } + +type Withdrawal struct { + Id int64 `gorm:"primaryKey"` + UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"` + Amount int64 `gorm:"not null;comment:Withdrawal Amount"` + Content string `gorm:"type:text;comment:Withdrawal Content"` + Status uint8 `gorm:"type:tinyint(1);default:0;comment:Withdrawal Status: 0: Pending 1: Approved 2: Rejected"` + Reason string `gorm:"type:varchar(500);default:'';comment:Rejection Reason"` + CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +func (*Withdrawal) TableName() string { + return "user_withdrawal" +} diff --git a/internal/report/report.go b/internal/report/report.go new file mode 100644 index 0000000..1ba618a --- /dev/null +++ b/internal/report/report.go @@ -0,0 +1,15 @@ +package report + +const ( + RegisterAPI = "/basic/register" // 模块注册接口 +) + +// RegisterResponse 模块注册响应参数 +type RegisterResponse struct { + Code int `json:"code"` // 响应代码 + Message string `json:"message"` // 响应信息 + Data struct { + Success bool `json:"success"` // 注册是否成功 + Message string `json:"message"` // 返回信息 + } `json:"data"` // 响应数据 +} diff --git a/internal/report/tool.go b/internal/report/tool.go new file mode 100644 index 0000000..fe8a68c --- /dev/null +++ b/internal/report/tool.go @@ -0,0 +1,113 @@ +package report + +import ( + "fmt" + "net" + "os" + + "github.com/go-resty/resty/v2" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/logger" + "github.com/pkg/errors" +) + +// FreePort returns a free TCP port by opening a listener on port 0. +func FreePort() (int, error) { + l, err := net.Listen("tcp", ":0") + if err != nil { + return 0, err + } + defer l.Close() + // Get the assigned port + addr := l.Addr().(*net.TCPAddr) + return addr.Port, nil +} + +// ModulePort returns the module port from the environment variable or a free port. +func ModulePort() (int, error) { + // 从环境变量获取端口号 + value, exists := os.LookupEnv("PPANEL_PORT") + if exists { + var port int + _, err := fmt.Sscanf(value, "%d", &port) + if err != nil { + return FreePort() + } + return port, nil + } + return FreePort() +} + +// GatewayPort returns the gateway port from the environment variable or a free port. +func GatewayPort() (int, error) { + // 从环境变量获取端口号 + value, exists := os.LookupEnv("GATEWAY_PORT") + if exists { + var port int + _, err := fmt.Sscanf(value, "%d", &port) + if err != nil { + logger.Errorf("Failed to parse GATEWAY_PORT: %v Value %s", err.Error(), value) + panic(err) + } + return port, nil + } + return 0, errors.New("could not determine gateway port") +} + +// RegisterModule registers a module with the gateway. +func RegisterModule(port int) error { + // 从环境变量中读取网关模块端口 + gatewayPort, err := GatewayPort() + if err != nil { + logger.Errorf("Failed to determine GATEWAY_PORT: %v", err) + return err + } + + // 从环境变量中获取通讯密钥 + value, exists := os.LookupEnv("SECRET_KEY") + if !exists { + panic("could not determine secret key") + } + + var response RegisterResponse + + client := resty.New().SetBaseURL(fmt.Sprintf("http://127.0.0.1:%d", gatewayPort)) + result, err := client.R().SetHeader("Content-Type", "application/json").SetBody(RegisterServiceRequest{ + Secret: value, + ProxyPath: "/api", + ServiceURL: fmt.Sprintf("http://127.0.0.1:%d", port), + Repository: constant.Repository, + HeartbeatURL: fmt.Sprintf("http://127.0.0.1:%d/v1/common/heartbeat", port), + ServiceName: constant.ServiceName, + ServiceVersion: constant.Version, + }).SetResult(&response).Post(RegisterAPI) + + if err != nil { + logger.Errorf("Failed to register service: %v", err) + return err + } + + if result.IsError() { + return errors.New("failed to register module: " + result.Status()) + } + + if !response.Data.Success { + logger.Infof("Result: %v", result.String()) + return errors.New("failed to register module: " + response.Message) + } + logger.Infof("Module registered successfully: %s", response.Message) + return nil +} + +// IsGatewayMode checks if the application is running in gateway mode. +// It returns true if GATEWAY_MODE is set to "true" and GATEWAY_PORT is valid. +func IsGatewayMode() bool { + value, exists := os.LookupEnv("GATEWAY_MODE") + if exists && value == "true" { + if _, err := GatewayPort(); err == nil { + return true + } + } + + return false +} diff --git a/internal/report/tool_test.go b/internal/report/tool_test.go new file mode 100644 index 0000000..cee9d83 --- /dev/null +++ b/internal/report/tool_test.go @@ -0,0 +1,21 @@ +package report + +import ( + "testing" +) + +func TestFreePort(t *testing.T) { + port, err := FreePort() + if err != nil { + t.Fatalf("FreePort() error: %v", err) + } + t.Logf("FreePort: %v", port) +} + +func TestModulePort(t *testing.T) { + port, err := ModulePort() + if err != nil { + t.Fatalf("ModulePort() error: %v", err) + } + t.Logf("ModulePort: %v", port) +} diff --git a/internal/report/types.go b/internal/report/types.go new file mode 100644 index 0000000..d9cd643 --- /dev/null +++ b/internal/report/types.go @@ -0,0 +1,11 @@ +package report + +type RegisterServiceRequest struct { + Secret string `json:"secret"` // 通讯密钥 + ProxyPath string `json:"proxy_path"` // 代理路径 + ServiceURL string `json:"service_url"` // 服务地址 + Repository string `json:"repository"` // 服务代码仓库 + ServiceName string `json:"service_name"` // 服务名称 + HeartbeatURL string `json:"heartbeat_url"` // 心跳检测地址 + ServiceVersion string `json:"service_version"` // 服务版本 +} diff --git a/internal/server.go b/internal/server.go index 64c2704..78d6422 100644 --- a/internal/server.go +++ b/internal/server.go @@ -6,8 +6,10 @@ import ( "errors" "fmt" "net/http" + "os" "time" + "github.com/perfect-panel/server/internal/report" "github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/proc" @@ -48,7 +50,7 @@ func initServer(svc *svc.ServiceContext) *gin.Engine { } r.Use(sessions.Sessions("ppanel", sessionStore)) // use cors middleware - r.Use(middleware.TraceMiddleware(svc), middleware.LoggerMiddleware(svc), middleware.CorsMiddleware, middleware.PanDomainMiddleware(svc), gin.Recovery()) + r.Use(middleware.TraceMiddleware(svc), middleware.LoggerMiddleware(svc), middleware.CorsMiddleware, gin.Recovery()) // register handlers handler.RegisterHandlers(r, svc) @@ -65,9 +67,32 @@ func (m *Service) Start() { if m.svc == nil { panic("config file path is nil") } + // init service r := initServer(m.svc) - serverAddr := fmt.Sprintf("%v:%d", m.svc.Config.Host, m.svc.Config.Port) + // get server port + port := m.svc.Config.Port + host := m.svc.Config.Host + // check gateway mode + if report.IsGatewayMode() { + // get free port + freePort, err := report.ModulePort() + if err != nil { + logger.Errorf("get module port error: %s", err.Error()) + panic(err) + } + port = freePort + host = "127.0.0.1" + // register module + err = report.RegisterModule(port) + if err != nil { + logger.Errorf("register module error: %s", err.Error()) + os.Exit(1) + } + logger.Infof("module registered on port %d", port) + } + + serverAddr := fmt.Sprintf("%v:%d", host, port) m.server = &http.Server{ Addr: serverAddr, Handler: r, diff --git a/internal/svc/mmdb.go b/internal/svc/mmdb.go new file mode 100644 index 0000000..331034f --- /dev/null +++ b/internal/svc/mmdb.go @@ -0,0 +1,74 @@ +package svc + +import ( + "io" + "net/http" + "os" + "path/filepath" + + "github.com/oschwald/geoip2-golang" + "github.com/perfect-panel/server/pkg/logger" +) + +const GeoIPDBURL = "https://raw.githubusercontent.com/adysec/IP_database/main/geolite/GeoLite2-City.mmdb" + +type IPLocation struct { + Path string + DB *geoip2.Reader +} + +func NewIPLocation(path string) (*IPLocation, error) { + + // 检查文件是否存在 + if _, err := os.Stat(path); os.IsNotExist(err) { + logger.Infof("[GeoIP] Database not found, downloading from %s", GeoIPDBURL) + // 文件不存在,下载数据库 + err := DownloadGeoIPDatabase(GeoIPDBURL, path) + if err != nil { + logger.Errorf("[GeoIP] Failed to download database: %v", err.Error()) + return nil, err + } + logger.Infof("[GeoIP] Database downloaded successfully") + } + + db, err := geoip2.Open(path) + if err != nil { + return nil, err + } + return &IPLocation{ + Path: path, + DB: db, + }, nil +} + +func (ipLoc *IPLocation) Close() error { + return ipLoc.DB.Close() +} + +func DownloadGeoIPDatabase(url, path string) error { + + // 创建路径, 确保目录存在 + err := os.MkdirAll(filepath.Dir(path), 0755) + if err != nil { + logger.Errorf("[GeoIP] Failed to create directory: %v", err.Error()) + return err + } + + // 创建文件 + out, err := os.Create(path) + if err != nil { + return err + } + defer out.Close() + + // 请求远程文件 + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + // 保存文件 + _, err = io.Copy(out, resp.Body) + return err +} diff --git a/internal/svc/serviceContext.go b/internal/svc/serviceContext.go index f80c392..d7f41c3 100644 --- a/internal/svc/serviceContext.go +++ b/internal/svc/serviceContext.go @@ -4,8 +4,8 @@ import ( "context" "github.com/perfect-panel/server/internal/model/client" - "github.com/perfect-panel/server/internal/model/server" "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/model/server" "github.com/perfect-panel/server/pkg/device" "github.com/perfect-panel/server/internal/config" @@ -33,20 +33,23 @@ import ( ) type ServiceContext struct { - DB *gorm.DB - Redis *redis.Client - Config config.Config - Queue *asynq.Client + DB *gorm.DB + Redis *redis.Client + Config config.Config + Queue *asynq.Client + ExchangeRate float64 + GeoIP *IPLocation + //NodeCache *cache.NodeCacheClient - AuthModel auth.Model - AdsModel ads.Model - LogModel log.Model - NodeModel node.Model - UserModel user.Model - OrderModel order.Model - ClientModel client.Model - TicketModel ticket.Model - ServerModel server.Model + AuthModel auth.Model + AdsModel ads.Model + LogModel log.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 PaymentModel payment.Model @@ -67,9 +70,17 @@ func NewServiceContext(c config.Config) *ServiceContext { db, err := orm.ConnectMysql(orm.Mysql{ Config: c.MySQL, }) + if err != nil { panic(err.Error()) } + + // IP location initialize + geoIP, err := NewIPLocation("./cache/GeoLite2-City.mmdb") + if err != nil { + panic(err.Error()) + } + rds := redis.NewClient(&redis.Options{ Addr: c.Redis.Host, Password: c.Redis.Pass, @@ -83,20 +94,22 @@ func NewServiceContext(c config.Config) *ServiceContext { } authLimiter := limit.NewPeriodLimit(86400, 15, rds, config.SendCountLimitKeyPrefix, limit.Align()) srv := &ServiceContext{ - DB: db, - Redis: rds, - Config: c, - Queue: NewAsynqClient(c), + DB: db, + Redis: rds, + Config: c, + Queue: NewAsynqClient(c), + ExchangeRate: 1.0, + GeoIP: geoIP, //NodeCache: cache.NewNodeCacheClient(rds), - AuthLimiter: authLimiter, - AdsModel: ads.NewModel(db, rds), - LogModel: log.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), + AuthLimiter: authLimiter, + AdsModel: ads.NewModel(db, rds), + LogModel: log.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), ServerModel: server.NewModel(db, rds), SystemModel: system.NewModel(db, rds), CouponModel: coupon.NewModel(db, rds), diff --git a/internal/types/types.go b/internal/types/types.go index d2c5543..bfc174b 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -118,6 +118,7 @@ type ApplicationVersion struct { type AuthConfig struct { Mobile MobileAuthenticateConfig `json:"mobile"` Email EmailAuthticateConfig `json:"email"` + Device DeviceAuthticateConfig `json:"device"` Register PubilcRegisterConfig `json:"register"` } @@ -238,6 +239,11 @@ type CommissionLog struct { Timestamp int64 `json:"timestamp"` } +type CommissionWithdrawRequest struct { + Amount int64 `json:"amount"` + Content string `json:"content"` +} + type Coupon struct { Id int64 `json:"id"` Name string `json:"name"` @@ -518,6 +524,20 @@ type DeleteUserSubscribeRequest struct { UserSubscribeId int64 `json:"user_subscribe_id"` } +type DeviceAuthticateConfig struct { + Enable bool `json:"enable"` + ShowAds bool `json:"show_ads"` + EnableSecurity bool `json:"enable_security"` + OnlyRealDevice bool `json:"only_real_device"` +} + +type DeviceLoginRequest struct { + Identifier string `json:"identifier" validate:"required"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `json:"user_agent" validate:"required"` + CfToken string `json:"cf_token,optional"` +} + type Document struct { Id int64 `json:"id"` Title string `json:"title"` @@ -803,6 +823,11 @@ type GetDetailRequest struct { Id int64 `form:"id" validate:"required"` } +type GetDeviceListResponse struct { + List []UserDevice `json:"list"` + Total int64 `json:"total"` +} + type GetDocumentDetailRequest struct { Id int64 `json:"id" validate:"required"` } @@ -929,7 +954,7 @@ type GetStatResponse struct { Node int64 `json:"node"` Country int64 `json:"country"` Protocol []string `json:"protocol"` - OnlineDevice int64 `json:"online_device"` + OnlineDevice int64 `json:"online_devices"` } type GetSubscribeApplicationListRequest struct { @@ -1142,6 +1167,12 @@ type HasMigrateSeverNodeResponse struct { HasMigrate bool `json:"has_migrate"` } +type HeartbeatResponse struct { + Status bool `json:"status"` + Message string `json:"message,omitempty"` + Timestamp int64 `json:"timestamp,omitempty"` +} + type Hysteria2 struct { Port int `json:"port" validate:"required"` HopPorts string `json:"hop_ports" validate:"required"` @@ -1151,10 +1182,9 @@ type Hysteria2 struct { } type InviteConfig struct { - ForcedInvite bool `json:"forced_invite"` - FirstPurchasePercentage int64 `json:"first_purchase_percentage"` - FirstYearlyPurchasePercentage int64 `json:"first_yearly_purchase_percentage"` - NonFirstPurchasePercentage int64 `json:"non_first_purchase_percentage"` + ForcedInvite bool `json:"forced_invite"` + ReferralPercentage int64 `json:"referral_percentage"` + OnlyFirstPurchase bool `json:"only_first_purchase"` } type KickOfflineRequest struct { @@ -1206,6 +1236,12 @@ type MobileAuthenticateConfig struct { Whitelist []string `json:"whitelist"` } +type ModuleConfig struct { + Secret string `json:"secret"` // 通讯密钥 + ServiceName string `json:"service_name"` // 服务名称 + ServiceVersion string `json:"service_version"` // 服务版本 +} + type Node struct { Id int64 `json:"id"` Name string `json:"name"` @@ -1525,7 +1561,7 @@ type PurchaseOrderResponse struct { type QueryAnnouncementRequest struct { Page int `form:"page"` - Size int `form:"size"` + Size int `form:"size,default=15"` Pinned *bool `form:"pinned"` Popup *bool `form:"popup"` } @@ -1544,6 +1580,16 @@ type QueryDocumentListResponse struct { List []Document `json:"list"` } +type QueryIPLocationRequest struct { + IP string `form:"ip" validate:"required"` +} + +type QueryIPLocationResponse struct { + Country string `json:"country"` + Region string `json:"region,omitempty"` + City string `json:"city"` +} + type QueryNodeTagResponse struct { Tags []string `json:"tags"` } @@ -1687,6 +1733,20 @@ type QueryUserSubscribeListResponse struct { Total int64 `json:"total"` } +type QueryUserSubscribeNodeListResponse struct { + List []UserSubscribeInfo `json:"list"` +} + +type QueryWithdrawalLogListRequest struct { + Page int `form:"page"` + Size int `form:"size"` +} + +type QueryWithdrawalLogListResponse struct { + List []WithdrawalLog `json:"list"` + Total int64 `json:"total"` +} + type QuotaTask struct { Id int64 `json:"id"` Subscribers []int64 `json:"subscribers"` @@ -1746,13 +1806,19 @@ type RenewalOrderResponse struct { OrderNo string `json:"order_no"` } +type ResetAllSubscribeTokenResponse struct { + Success bool `json:"success"` +} + type ResetPasswordRequest struct { - Email string `json:"email" validate:"required"` - Password string `json:"password" validate:"required"` - Code string `json:"code,optional"` - IP string `header:"X-Original-Forwarded-For"` - UserAgent string `header:"User-Agent"` - CfToken string `json:"cf_token,optional"` + Identifier string `json:"identifier"` + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + Code string `json:"code,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 ResetSortRequest struct { @@ -2003,8 +2069,8 @@ type Subscribe struct { Quota int64 `json:"quota"` Nodes []int64 `json:"nodes"` NodeTags []string `json:"node_tags"` + ServerGroup string `json:"server_group"` ServerCount int64 `json:"server_count"` - Server []int64 `json:"server"` Show bool `json:"show"` Sell bool `json:"sell"` Sort int64 `json:"sort"` @@ -2102,16 +2168,19 @@ type TelephoneCheckUserResponse struct { } type TelephoneLoginRequest struct { + Identifier string `json:"identifier"` Telephone string `json:"telephone" validate:"required"` TelephoneCode string `json:"telephone_code"` TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` Password string `json:"password"` IP string `header:"X-Original-Forwarded-For"` UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type"` CfToken string `json:"cf_token,optional"` } type TelephoneRegisterRequest struct { + Identifier string `json:"identifier"` Telephone string `json:"telephone" validate:"required"` TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` Password string `json:"password" validate:"required"` @@ -2119,16 +2188,19 @@ type TelephoneRegisterRequest struct { Code string `json:"code,optional"` IP string `header:"X-Original-Forwarded-For"` UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type,optional"` CfToken string `json:"cf_token,optional"` } type TelephoneResetPasswordRequest struct { + Identifier string `json:"identifier"` Telephone string `json:"telephone" validate:"required"` TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` Password string `json:"password" validate:"required"` Code string `json:"code,optional"` IP string `header:"X-Original-Forwarded-For"` UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type,optional"` CfToken string `json:"cf_token,optional"` } @@ -2224,6 +2296,10 @@ type Tuic struct { SecurityConfig SecurityConfig `json:"security_config"` } +type UnbindDeviceRequest struct { + Id int64 `json:"id" validate:"required"` +} + type UnbindOAuthRequest struct { Method string `json:"method"` } @@ -2430,6 +2506,15 @@ type UpdateUserPasswordRequest struct { Password string `json:"password" validate:"required"` } +type UpdateUserRulesRequest struct { + Rules []string `json:"rules" validate:"required"` +} + +type UpdateUserSubscribeNoteRequest struct { + UserSubscribeId int64 `json:"user_subscribe_id" validate:"required"` + Note string `json:"note" validate:"max=500"` +} + type UpdateUserSubscribeRequest struct { UserSubscribeId int64 `json:"user_subscribe_id"` SubscribeId int64 `json:"subscribe_id"` @@ -2464,6 +2549,7 @@ type User struct { EnableTradeNotify bool `json:"enable_trade_notify"` AuthMethods []UserAuthMethod `json:"auth_methods"` UserDevices []UserDevice `json:"user_devices"` + Rules []string `json:"rules"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` DeletedAt int64 `json:"deleted_at,omitempty"` @@ -2504,21 +2590,25 @@ type UserLoginLog struct { } type UserLoginRequest struct { - Email string `json:"email" validate:"required"` - Password string `json:"password" validate:"required"` - IP string `header:"X-Original-Forwarded-For"` - UserAgent string `header:"User-Agent"` - CfToken string `json:"cf_token,optional"` + Identifier string `json:"identifier"` + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type"` + CfToken string `json:"cf_token,optional"` } type UserRegisterRequest struct { - Email string `json:"email" validate:"required"` - Password string `json:"password" validate:"required"` - Invite string `json:"invite,optional"` - Code string `json:"code,optional"` - IP string `header:"X-Original-Forwarded-For"` - UserAgent string `header:"User-Agent"` - CfToken string `json:"cf_token,optional"` + Identifier string `json:"identifier"` + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + Invite string `json:"invite,optional"` + Code string `json:"code,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 UserStatistics struct { @@ -2550,6 +2640,7 @@ type UserSubscribe struct { Upload int64 `json:"upload"` Token string `json:"token"` Status uint8 `json:"status"` + Short string `json:"short"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } @@ -2573,6 +2664,26 @@ type UserSubscribeDetail struct { UpdatedAt int64 `json:"updated_at"` } +type UserSubscribeInfo struct { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + OrderId int64 `json:"order_id"` + SubscribeId int64 `json:"subscribe_id"` + StartTime int64 `json:"start_time"` + ExpireTime int64 `json:"expire_time"` + FinishedAt int64 `json:"finished_at"` + ResetTime int64 `json:"reset_time"` + Traffic int64 `json:"traffic"` + Download int64 `json:"download"` + Upload int64 `json:"upload"` + Token string `json:"token"` + Status uint8 `json:"status"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + IsTryOut bool `json:"is_try_out"` + Nodes []*UserSubscribeNodeInfo `json:"nodes"` +} + type UserSubscribeLog struct { Id int64 `json:"id"` UserId int64 `json:"user_id"` @@ -2583,6 +2694,19 @@ type UserSubscribeLog struct { Timestamp int64 `json:"timestamp"` } +type UserSubscribeNodeInfo struct { + Id int64 `json:"id"` + Name string `json:"name"` + Uuid string `json:"uuid"` + Protocol string `json:"protocol"` + Port uint16 `json:"port"` + Address string `json:"address"` + Tags []string `json:"tags"` + Country string `json:"country"` + City string `json:"city"` + CreatedAt int64 `json:"created_at"` +} + type UserSubscribeTrafficLog struct { SubscribeId int64 `json:"subscribe_id"` // Subscribe ID UserId int64 `json:"user_id"` // User ID @@ -2670,3 +2794,14 @@ type VmessProtocol struct { Network string `json:"network"` Transport string `json:"transport"` } + +type WithdrawalLog struct { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + Amount int64 `json:"amount"` + Content string `json:"content"` + Status uint8 `json:"status"` + Reason string `json:"reason,omitempty"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} diff --git a/pkg/conf/default.go b/pkg/conf/default.go index 9c6af19..e1b1760 100644 --- a/pkg/conf/default.go +++ b/pkg/conf/default.go @@ -56,6 +56,10 @@ func parseDefaultValue(kind reflect.Kind, defaultValue string) any { var i uint32 _, _ = fmt.Sscanf(defaultValue, "%d", &i) return i + case reflect.Uint8: + var i uint8 + _, _ = fmt.Sscanf(defaultValue, "%d", &i) + return i default: fmt.Printf("类型 %v 没有处理, 值为: %v \n", kind, defaultValue) panic("unhandled default case") diff --git a/pkg/constant/context.go b/pkg/constant/context.go index 4023cd1..45c7f86 100644 --- a/pkg/constant/context.go +++ b/pkg/constant/context.go @@ -8,4 +8,5 @@ const ( CtxKeyRequestHost CtxKey = "requestHost" CtxKeyPlatform CtxKey = "platform" CtxKeyPayment CtxKey = "payment" + LoginType CtxKey = "loginType" ) diff --git a/pkg/constant/version.go b/pkg/constant/version.go index 5370107..b9e58ed 100644 --- a/pkg/constant/version.go +++ b/pkg/constant/version.go @@ -2,6 +2,8 @@ package constant // Version PPanel version var ( - Version = "unknown version" - BuildTime = "unknown time" + Version = "unknown version" + BuildTime = "unknown time" + Repository = "https://github.com/perfect-panel/server" + ServiceName = "ApiService" ) diff --git a/pkg/payment/epay/epay.go b/pkg/payment/epay/epay.go index ae1315e..8933d9f 100644 --- a/pkg/payment/epay/epay.go +++ b/pkg/payment/epay/epay.go @@ -14,9 +14,10 @@ import ( ) type Client struct { - Pid string - Url string - Key string + Pid string + Url string + Key string + Type string } type Order struct { @@ -37,11 +38,12 @@ type queryOrderStatusResponse struct { Status int `json:"status"` } -func NewClient(pid, url, key string) *Client { +func NewClient(pid, url, key string, Type string) *Client { return &Client{ - Pid: pid, - Url: url, - Key: key, + Pid: pid, + Url: url, + Key: key, + Type: Type, } } @@ -53,6 +55,7 @@ func (c *Client) CreatePayUrl(order Order) string { params.Set("notify_url", order.NotifyUrl) params.Set("out_trade_no", order.OrderNo) params.Set("pid", c.Pid) + params.Set("type", c.Type) params.Set("return_url", order.ReturnUrl) // Generate the sign using the CreateSign function @@ -117,6 +120,7 @@ func (c *Client) structToMap(order Order) map[string]string { result["notify_url"] = order.NotifyUrl result["out_trade_no"] = order.OrderNo result["pid"] = c.Pid + result["type"] = c.Type result["return_url"] = order.ReturnUrl return result } diff --git a/pkg/payment/epay/epay_test.go b/pkg/payment/epay/epay_test.go index a3c6884..87265e6 100644 --- a/pkg/payment/epay/epay_test.go +++ b/pkg/payment/epay/epay_test.go @@ -3,7 +3,7 @@ package epay import "testing" func TestEpay(t *testing.T) { - client := NewClient("", "http://127.0.0.1", "") + client := NewClient("", "http://127.0.0.1", "", "") order := Order{ Name: "测试", OrderNo: "123456789", @@ -19,7 +19,7 @@ func TestEpay(t *testing.T) { func TestQueryOrderStatus(t *testing.T) { t.Skipf("Skip TestQueryOrderStatus test") - client := NewClient("Pid", "Url", "Key") + client := NewClient("Pid", "Url", "Key", "Type") orderNo := "123456789" status := client.QueryOrderStatus(orderNo) t.Logf("OrderNo: %s, Status: %v\n", orderNo, status) @@ -40,7 +40,7 @@ func TestVerifySign(t *testing.T) { } key := "LbTabbB580zWyhXhyyww7wwvy5u8k0wl" - c := NewClient("Pid", "Url", key) + c := NewClient("Pid", "Url", key, "Type") if c.VerifySign(params) { t.Logf("Sign verification success!") } else { diff --git a/pkg/payment/platform.go b/pkg/payment/platform.go index 42b8815..7ad12ea 100644 --- a/pkg/payment/platform.go +++ b/pkg/payment/platform.go @@ -65,9 +65,10 @@ func GetSupportedPlatforms() []types.PlatformInfo { Platform: EPay.String(), PlatformUrl: "", PlatformFieldDescription: map[string]string{ - "pid": "PID", - "url": "URL", - "key": "Key", + "pid": "PID", + "url": "URL", + "key": "Key", + "type": "Type", }, }, { diff --git a/pkg/tool/encryption.go b/pkg/tool/encryption.go index e4f205e..f51f61a 100644 --- a/pkg/tool/encryption.go +++ b/pkg/tool/encryption.go @@ -2,12 +2,14 @@ package tool import ( "crypto/md5" + "crypto/sha256" "crypto/sha512" "encoding/hex" "fmt" "strings" "github.com/anaskhan96/go-password-encoder" + "golang.org/x/crypto/bcrypt" ) var options = &password.Options{SaltLen: 16, Iterations: 100, KeyLen: 32, HashFunction: sha512.New} @@ -32,3 +34,24 @@ func Md5Encode(str string, isUpper bool) string { } return res } + +func MultiPasswordVerify(algo, salt, password, hash string) bool { + switch algo { + case "md5": + sum := md5.Sum([]byte(password)) + return hex.EncodeToString(sum[:]) == hash + case "sha256": + sum := sha256.Sum256([]byte(password)) + return hex.EncodeToString(sum[:]) == hash + case "md5salt": + sum := md5.Sum([]byte(password + salt)) + return hex.EncodeToString(sum[:]) == hash + case "default": // PPanel's default algorithm + return VerifyPassWord(password, hash) + case "bcrypt": + // Bcrypt (corresponding to PHP's password_hash/password_verify) + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil + } + return false +} diff --git a/pkg/tool/encryption_test.go b/pkg/tool/encryption_test.go index 45e0a18..8e420cf 100644 --- a/pkg/tool/encryption_test.go +++ b/pkg/tool/encryption_test.go @@ -1,7 +1,15 @@ package tool -import "testing" +import ( + "testing" +) func TestEncodePassWord(t *testing.T) { - t.Logf("EncodePassWord: %v", EncodePassWord("")) + t.Logf("EncodePassWord: %v", EncodePassWord("password")) +} + +func TestMultiPasswordVerify(t *testing.T) { + pwd := "$2y$10$WFO17pdtohfeBILjEChoGeVxpDG.u9kVCKhjDAeEeNmCjIlj3tDRy" + status := MultiPasswordVerify("bcrypt", "", "admin1", pwd) + t.Logf("MultiPasswordVerify: %v", status) } diff --git a/pkg/tool/string.go b/pkg/tool/string.go new file mode 100644 index 0000000..2567dc0 --- /dev/null +++ b/pkg/tool/string.go @@ -0,0 +1,38 @@ +package tool + +import ( + "crypto/sha256" + "encoding/binary" + "errors" + "math/rand" +) + +func FixedUniqueString(s string, length int, alphabet string) (string, error) { + if alphabet == "" { + alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + } + if length <= 0 { + return "", errors.New("length must be > 0") + } + if length > len(alphabet) { + return "", errors.New("length greater than available unique characters") + } + + // Generate deterministic seed from SHA256 + hash := sha256.Sum256([]byte(s)) + seed := int64(binary.LittleEndian.Uint64(hash[:8])) // 前 8 字节 + + r := rand.New(rand.NewSource(seed)) + + // Copy alphabet to mutable array + data := []rune(alphabet) + + // Deterministic shuffle (Fisher–Yates) + for i := len(data) - 1; i > 0; i-- { + j := r.Intn(i + 1) + data[i], data[j] = data[j], data[i] + } + + // Take first N characters + return string(data[:length]), nil +} diff --git a/pkg/tool/string_test.go b/pkg/tool/string_test.go new file mode 100644 index 0000000..0c44086 --- /dev/null +++ b/pkg/tool/string_test.go @@ -0,0 +1,27 @@ +package tool + +import ( + "testing" +) + +func TestFixedUniqueString(t *testing.T) { + a := "example" + b := "example1" + c := "example" + + strA1, err := FixedUniqueString(a, 8, "") + strB1, err := FixedUniqueString(b, 8, "") + strC1, err := FixedUniqueString(c, 8, "") + if err != nil { + t.Logf("Error: %v", err.Error()) + return + } + if strA1 != strC1 { + t.Errorf("Expected strA1 and strC1 to be equal, got %s and %s", strA1, strC1) + } + if strA1 == strB1 { + t.Errorf("Expected strA1 and strB1 to be different, got %s and %s", strA1, strB1) + } + t.Logf("strA1 and strB1 are not equal, strA1: %s, strB1: %s", strA1, strB1) + t.Logf("strA1 and strC1 are equal,strA1: %s, strC1: %s", strA1, strC1) +} diff --git a/pkg/xerr/errCode.go b/pkg/xerr/errCode.go index 64cbd6a..7e1bfd3 100644 --- a/pkg/xerr/errCode.go +++ b/pkg/xerr/errCode.go @@ -18,15 +18,16 @@ const ( // User error const ( - UserExist uint32 = 20001 - UserNotExist uint32 = 20002 - UserPasswordError uint32 = 20003 - UserDisabled uint32 = 20004 - InsufficientBalance uint32 = 20005 - StopRegister uint32 = 20006 - TelegramNotBound uint32 = 20007 - UserNotBindOauth uint32 = 20008 - InviteCodeError uint32 = 20009 + UserExist uint32 = 20001 + UserNotExist uint32 = 20002 + UserPasswordError uint32 = 20003 + UserDisabled uint32 = 20004 + InsufficientBalance uint32 = 20005 + StopRegister uint32 = 20006 + TelegramNotBound uint32 = 20007 + UserNotBindOauth uint32 = 20008 + InviteCodeError uint32 = 20009 + UserCommissionNotEnough uint32 = 20010 ) // Node error @@ -47,6 +48,7 @@ const ( ErrorTokenExpire uint32 = 40004 InvalidAccess uint32 = 40005 InvalidCiphertext uint32 = 40006 + SecretIsEmpty uint32 = 40007 ) //coupon error @@ -56,6 +58,7 @@ const ( CouponAlreadyUsed uint32 = 50002 // Coupon has already been used CouponNotApplicable uint32 = 50003 // Coupon does not match the order or conditions CouponInsufficientUsage uint32 = 50004 // Coupon has insufficient remaining uses + CouponExpired uint32 = 50005 // Coupon is expired ) // Subscribe diff --git a/pkg/xerr/errMsg.go b/pkg/xerr/errMsg.go index a259e54..f688854 100644 --- a/pkg/xerr/errMsg.go +++ b/pkg/xerr/errMsg.go @@ -14,6 +14,7 @@ func init() { ErrorTokenEmpty: "User token is empty", ErrorTokenInvalid: "User token is invalid", ErrorTokenExpire: "User token is expired", + SecretIsEmpty: "Secret is empty", InvalidAccess: "Invalid access", InvalidCiphertext: "Invalid ciphertext", // Database error @@ -45,6 +46,7 @@ func init() { CouponAlreadyUsed: "Coupon has already been used", CouponNotApplicable: "Coupon does not match the order or conditions", CouponInsufficientUsage: "Coupon has insufficient remaining uses", + CouponExpired: "Coupon is expired", // Subscribe SubscribeExpired: "Subscribe is expired", diff --git a/queue/handler/routes.go b/queue/handler/routes.go index edf2293..2e96219 100644 --- a/queue/handler/routes.go +++ b/queue/handler/routes.go @@ -3,7 +3,6 @@ package handler import ( "github.com/hibiken/asynq" "github.com/perfect-panel/server/internal/svc" - countrylogic "github.com/perfect-panel/server/queue/logic/country" orderLogic "github.com/perfect-panel/server/queue/logic/order" smslogic "github.com/perfect-panel/server/queue/logic/sms" "github.com/perfect-panel/server/queue/logic/subscription" @@ -15,8 +14,6 @@ import ( ) func RegisterHandlers(mux *asynq.ServeMux, serverCtx *svc.ServiceContext) { - // get country task - mux.Handle(types.ForthwithGetCountry, countrylogic.NewGetNodeCountryLogic(serverCtx)) // Send email task mux.Handle(types.ForthwithSendEmail, emailLogic.NewSendEmailLogic(serverCtx)) // Send sms task diff --git a/queue/logic/country/getCountryLogic.go b/queue/logic/country/getCountryLogic.go deleted file mode 100644 index 75e0f6f..0000000 --- a/queue/logic/country/getCountryLogic.go +++ /dev/null @@ -1,22 +0,0 @@ -package countrylogic - -import ( - "context" - - "github.com/hibiken/asynq" - "github.com/perfect-panel/server/internal/svc" -) - -type GetNodeCountryLogic struct { - svcCtx *svc.ServiceContext -} - -func NewGetNodeCountryLogic(svcCtx *svc.ServiceContext) *GetNodeCountryLogic { - return &GetNodeCountryLogic{ - svcCtx: svcCtx, - } -} -func (l *GetNodeCountryLogic) ProcessTask(ctx context.Context, task *asynq.Task) error { - - return nil -} diff --git a/queue/logic/order/activateOrderLogic.go b/queue/logic/order/activateOrderLogic.go index 7e91493..55dc284 100644 --- a/queue/logic/order/activateOrderLogic.go +++ b/queue/logic/order/activateOrderLogic.go @@ -7,11 +7,9 @@ import ( "encoding/json" "fmt" "strconv" - "strings" "time" "github.com/perfect-panel/server/internal/model/log" - "github.com/perfect-panel/server/internal/model/node" "github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/pkg/logger" @@ -225,6 +223,7 @@ func (l *ActivateOrderLogic) createGuestUser(ctx context.Context, orderInfo *ord userInfo := &user.User{ Password: tool.EncodePassWord(tempOrder.Password), + Algo: "default", AuthMethods: []user.AuthMethods{ { AuthType: tempOrder.AuthType, @@ -466,17 +465,8 @@ func (l *ActivateOrderLogic) calculateCommission(price int64, percentage uint8) // clearServerCache clears user list cache for all servers associated with the subscription func (l *ActivateOrderLogic) clearServerCache(ctx context.Context, sub *subscribe.Subscribe) { - nodeIds := tool.StringToInt64Slice(sub.Nodes) - tags := strings.Split(sub.NodeTags, ",") - - err := l.svc.NodeModel.ClearNodeCache(ctx, &node.FilterNodeParams{ - Page: 1, - Size: 1000, - NodeId: nodeIds, - Tag: tags, - }) - if err != nil { - logger.WithContext(ctx).Error("[Order Queue] Clear node cache failed", logger.Field("error", err.Error())) + if err := l.svc.SubscribeModel.ClearCache(ctx, sub.Id); err != nil { + logger.WithContext(ctx).Error("[Order Queue] Clear subscribe cache failed", logger.Field("error", err.Error())) } } diff --git a/queue/logic/subscription/checkSubscriptionLogic.go b/queue/logic/subscription/checkSubscriptionLogic.go index c331de8..81b86e7 100644 --- a/queue/logic/subscription/checkSubscriptionLogic.go +++ b/queue/logic/subscription/checkSubscriptionLogic.go @@ -3,11 +3,8 @@ package subscription import ( "context" "encoding/json" - "strings" "time" - "github.com/perfect-panel/server/internal/model/node" - "github.com/perfect-panel/server/pkg/tool" queue "github.com/perfect-panel/server/queue/types" "github.com/perfect-panel/server/pkg/logger" @@ -33,7 +30,7 @@ func (l *CheckSubscriptionLogic) ProcessTask(ctx context.Context, _ *asynq.Task) // Check subscription traffic err := l.svc.UserModel.Transaction(ctx, func(db *gorm.DB) error { var list []*user.Subscribe - err := db.Model(&user.Subscribe{}).Where("upload + download >= traffic AND status = 1 AND traffic > 0 ").Find(&list).Error + err := db.Model(&user.Subscribe{}).Where("upload + download >= traffic AND status IN (0, 1) AND traffic > 0 ").Find(&list).Error if err != nil { logger.Errorw("[Check Subscription Traffic] Query subscribe failed", logger.Field("error", err.Error())) return err @@ -78,7 +75,7 @@ func (l *CheckSubscriptionLogic) ProcessTask(ctx context.Context, _ *asynq.Task) // Check subscription expire err = l.svc.UserModel.Transaction(ctx, func(db *gorm.DB) error { var list []*user.Subscribe - err = db.Model(&user.Subscribe{}).Where("`status` = 1 AND `expire_time` < ? AND `expire_time` != ? and `finished_at` IS NULL", time.Now(), time.UnixMilli(0)).Find(&list).Error + err = db.Model(&user.Subscribe{}).Where("`status` IN (0, 1) AND `expire_time` < ? AND `expire_time` != ? and `finished_at` IS NULL", time.Now(), time.UnixMilli(0)).Find(&list).Error if err != nil { logger.Error("[Check Subscription] Find subscribe failed", logger.Field("error", err.Error())) return err @@ -207,31 +204,8 @@ func (l *CheckSubscriptionLogic) clearServerCache(ctx context.Context, userSubs } for sub, _ := range subs { - info, err := l.svc.SubscribeModel.FindOne(ctx, sub) - if err != nil { - logger.Errorw("[CheckSubscription] FindOne subscribe failed", logger.Field("error", err.Error()), logger.Field("subscribe_id", sub)) - continue - } - if info != nil && info.Id == sub { - var nodes []int64 - if info.Nodes != "" { - nodes = tool.StringToInt64Slice(info.Nodes) - } - var tag []string - if info.NodeTags != "" { - tag = strings.Split(info.NodeTags, ",") - } - - err = l.svc.NodeModel.ClearNodeCache(ctx, &node.FilterNodeParams{ - Page: 1, - Size: 1000, - Tag: tag, - ServerId: nodes, - }) - if err != nil { - logger.Errorw("[CheckSubscription] ClearNodeCache failed", logger.Field("error", err.Error()), logger.Field("subscribe_id", sub)) - continue - } + if err := l.svc.SubscribeModel.ClearCache(ctx, sub); err != nil { + logger.Errorw("[CheckSubscription] ClearCache failed", logger.Field("error", err.Error()), logger.Field("subscribe_id", sub)) } } } diff --git a/queue/logic/task/rateLogic.go b/queue/logic/task/rateLogic.go new file mode 100644 index 0000000..2e33fae --- /dev/null +++ b/queue/logic/task/rateLogic.go @@ -0,0 +1,52 @@ +package task + +import ( + "context" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/exchangeRate" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" +) + +type RateLogic struct { + svcCtx *svc.ServiceContext +} + +func NewRateLogic(svcCtx *svc.ServiceContext) *RateLogic { + return &RateLogic{ + svcCtx: svcCtx, + } +} + +func (l *RateLogic) ProcessTask(ctx context.Context, _ *asynq.Task) error { + // Retrieve system currency configuration + currency, err := l.svcCtx.SystemModel.GetCurrencyConfig(ctx) + if err != nil { + logger.Errorw("[PurchaseCheckout] GetCurrencyConfig error", logger.Field("error", err.Error())) + return err + } + // Parse currency configuration + configs := struct { + CurrencyUnit string + CurrencySymbol string + AccessKey string + }{} + tool.SystemConfigSliceReflectToStruct(currency, &configs) + + // Skip conversion if no exchange rate API key configured + if configs.AccessKey == "" { + logger.Debugf("[RateLogic] skip exchange rate, no access key configured") + return nil + } + // Update exchange rates + result, err := exchangeRate.GetExchangeRete(configs.CurrencyUnit, "CNY", configs.AccessKey, 1) + if err != nil { + logger.Errorw("[RateLogic] GetExchangeRete error", logger.Field("error", err.Error())) + return err + } + l.svcCtx.ExchangeRate = result + logger.WithContext(ctx).Infof("[RateLogic] GetExchangeRete success, result: %+v", result) + return nil +} diff --git a/queue/logic/traffic/resetTrafficLogic.go b/queue/logic/traffic/resetTrafficLogic.go index 8f4f525..bbfee15 100644 --- a/queue/logic/traffic/resetTrafficLogic.go +++ b/queue/logic/traffic/resetTrafficLogic.go @@ -9,12 +9,10 @@ import ( "time" "github.com/perfect-panel/server/internal/model/log" - "github.com/perfect-panel/server/internal/model/node" "github.com/perfect-panel/server/internal/model/subscribe" "github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/server/pkg/tool" "github.com/perfect-panel/server/queue/types" "github.com/hibiken/asynq" @@ -598,31 +596,11 @@ func (l *ResetTrafficLogic) clearCache(ctx context.Context, list []*user.Subscri } for sub, _ := range subs { - info, err := l.svc.SubscribeModel.FindOne(ctx, sub) - if err != nil { - logger.Errorw("[CheckSubscription] FindOne subscribe failed", logger.Field("error", err.Error()), logger.Field("subscribe_id", sub)) - continue - } - if info != nil && info.Id == sub { - var nodes []int64 - if info.Nodes != "" { - nodes = tool.StringToInt64Slice(info.Nodes) - } - var tag []string - if info.NodeTags != "" { - tag = strings.Split(info.NodeTags, ",") - } - - err = l.svc.NodeModel.ClearNodeCache(ctx, &node.FilterNodeParams{ - Page: 1, - Size: 1000, - Tag: tag, - ServerId: nodes, - }) - if err != nil { - logger.Errorw("[CheckSubscription] ClearNodeCache failed", logger.Field("error", err.Error()), logger.Field("subscribe_id", sub)) - continue - } + if err := l.svc.SubscribeModel.ClearCache(ctx, sub); err != nil { + logger.Errorw("[ResetTraffic] Failed to clear subscription cache", + logger.Field("subscribeId", sub), + logger.Field("error", err.Error()), + ) } } } diff --git a/queue/logic/traffic/trafficStatLogic.go b/queue/logic/traffic/trafficStatLogic.go index 81a422b..e5357a8 100644 --- a/queue/logic/traffic/trafficStatLogic.go +++ b/queue/logic/traffic/trafficStatLogic.go @@ -167,7 +167,7 @@ func (l *StatLogic) ProcessTask(ctx context.Context, _ *asynq.Task) error { // Delete old traffic logs if l.svc.Config.Log.AutoClear { - err = tx.WithContext(ctx).Model(&traffic.TrafficLog{}).Where("created_at <= ?", end.AddDate(0, 0, int(-l.svc.Config.Log.ClearDays))).Delete(&traffic.TrafficLog{}).Error + err = tx.WithContext(ctx).Model(&traffic.TrafficLog{}).Where("timestamp <= ?", end.AddDate(0, 0, int(-l.svc.Config.Log.ClearDays))).Delete(&traffic.TrafficLog{}).Error if err != nil { logger.Errorf("[Traffic Stat Queue] Delete server traffic log failed: %v", err.Error()) } diff --git a/queue/types/country.go b/queue/types/country.go deleted file mode 100644 index 13b9e0c..0000000 --- a/queue/types/country.go +++ /dev/null @@ -1,11 +0,0 @@ -package types - -const ( - // ForthwithGetCountry forthwith country get - ForthwithGetCountry = "forthwith:country:get" -) - -type GetNodeCountry struct { - Protocol string `json:"protocol"` - ServerAddr string `json:"server_addr"` -} diff --git a/queue/types/task.go b/queue/types/task.go index 0115f28..85da331 100644 --- a/queue/types/task.go +++ b/queue/types/task.go @@ -6,4 +6,7 @@ const ( // ForthwithQuotaTask create quota task immediately ForthwithQuotaTask = "forthwith:quota:task" + + // SchedulerExchangeRate fetch exchange rate task + SchedulerExchangeRate = "scheduler:exchange:rate" ) diff --git a/readme_zh.md b/readme_zh.md index d2ff26e..ee4a1c4 100644 --- a/readme_zh.md +++ b/readme_zh.md @@ -113,8 +113,8 @@ PPanel 服务端是 PPanel 项目的后端组件,为代理服务提供强大 4. **从 Docker Hub 拉取**(CI/CD 发布后): ```bash - docker pull yourusername/ppanel-server:latest - docker run --rm -p 8080:8080 yourusername/ppanel-server:latest + docker pull ppanel/ppanel-server:latest + docker run --rm -p 8080:8080 ppanel/ppanel-server:latest ``` ## 📖 API 文档 diff --git a/scheduler/scheduler.go b/scheduler/scheduler.go index 377dca3..bf131e8 100644 --- a/scheduler/scheduler.go +++ b/scheduler/scheduler.go @@ -46,6 +46,12 @@ func (m *Service) Start() { logger.Errorf("register traffic stat task failed: %s", err.Error()) } + // schedule update exchange rate task: every day at 01:00 + rateTask := asynq.NewTask(types.ForthwithQuotaTask, nil) + if _, err := m.server.Register("0 1 * * *", rateTask, asynq.MaxRetry(3)); err != nil { + logger.Errorf("register update exchange rate task failed: %s", err.Error()) + } + if err := m.server.Run(); err != nil { logger.Errorf("run scheduler failed: %s", err.Error()) }