Merge branch 'old' into main
Some checks failed
Build docker and publish / build (20.15.1) (push) Has been cancelled

This commit is contained in:
shanshanzhong 2025-12-28 17:27:43 -08:00
commit 544cfb5c99
128 changed files with 3389 additions and 340 deletions

View File

@ -127,10 +127,28 @@ func (adapter *Adapter) Proxies(servers []*node.Node) ([]Proxy, error) {
HopPorts: protocol.HopPorts, HopPorts: protocol.HopPorts,
HopInterval: protocol.HopInterval, HopInterval: protocol.HopInterval,
ObfsPassword: protocol.ObfsPassword, ObfsPassword: protocol.ObfsPassword,
UpMbps: protocol.UpMbps,
DownMbps: protocol.DownMbps,
DisableSNI: protocol.DisableSNI, DisableSNI: protocol.DisableSNI,
ReduceRtt: protocol.ReduceRtt, ReduceRtt: protocol.ReduceRtt,
UDPRelayMode: protocol.UDPRelayMode, UDPRelayMode: protocol.UDPRelayMode,
CongestionController: protocol.CongestionController, 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,
}) })
} }
} }

View File

@ -102,6 +102,9 @@ type (
BatchDeleteSubscribeRequest { BatchDeleteSubscribeRequest {
Ids []int64 `json:"ids" validate:"required"` Ids []int64 `json:"ids" validate:"required"`
} }
ResetAllSubscribeTokenResponse {
Success bool `json:"success"`
}
) )
@server ( @server (
@ -157,5 +160,9 @@ service ppanel {
@doc "Subscribe sort" @doc "Subscribe sort"
@handler SubscribeSort @handler SubscribeSort
post /sort (SubscribeSortRequest) post /sort (SubscribeSortRequest)
@doc "Reset all subscribe tokens"
@handler ResetAllSubscribeToken
post /reset_all_token returns (ResetAllSubscribeTokenResponse)
} }

View File

@ -22,6 +22,11 @@ type (
CurrentTime string `json:"current_time"` CurrentTime string `json:"current_time"`
Ratio float32 `json:"ratio"` Ratio float32 `json:"ratio"`
} }
ModuleConfig {
Secret string `json:"secret"` // 通讯密钥
ServiceName string `json:"service_name"` // 服务名称
ServiceVersion string `json:"service_version"` // 服务版本
}
) )
@server ( @server (
@ -125,5 +130,9 @@ service ppanel {
@doc "PreView Node Multiplier" @doc "PreView Node Multiplier"
@handler PreViewNodeMultiplier @handler PreViewNodeMultiplier
get /node_multiplier/preview returns (PreViewNodeMultiplierResponse) get /node_multiplier/preview returns (PreViewNodeMultiplierResponse)
@doc "Get Module Config"
@handler GetModuleConfig
get /module returns (ModuleConfig)
} }

View File

@ -17,6 +17,14 @@ type (
VersionResponse { VersionResponse {
Version string `json:"version"` 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 ( @server (
@ -36,5 +44,9 @@ service ppanel {
@doc "Get Version" @doc "Get Version"
@handler GetVersion @handler GetVersion
get /version returns (VersionResponse) get /version returns (VersionResponse)
@doc "Query IP Location"
@handler QueryIPLocation
get /ip/location (QueryIPLocationRequest) returns (QueryIPLocationResponse)
} }

View File

@ -11,10 +11,12 @@ info (
type ( type (
// User login request // User login request
UserLoginRequest { UserLoginRequest {
Identifier string `json:"identifier"`
Email string `json:"email" validate:"required"` Email string `json:"email" validate:"required"`
Password string `json:"password" validate:"required"` Password string `json:"password" validate:"required"`
IP string `header:"X-Original-Forwarded-For"` IP string `header:"X-Original-Forwarded-For"`
UserAgent string `header:"User-Agent"` UserAgent string `header:"User-Agent"`
LoginType string `header:"Login-Type"`
CfToken string `json:"cf_token,optional"` CfToken string `json:"cf_token,optional"`
} }
// Check user is exist request // Check user is exist request
@ -27,21 +29,25 @@ type (
} }
// User login response // User login response
UserRegisterRequest { UserRegisterRequest {
Identifier string `json:"identifier"`
Email string `json:"email" validate:"required"` Email string `json:"email" validate:"required"`
Password string `json:"password" validate:"required"` Password string `json:"password" validate:"required"`
Invite string `json:"invite,optional"` Invite string `json:"invite,optional"`
Code string `json:"code,optional"` Code string `json:"code,optional"`
IP string `header:"X-Original-Forwarded-For"` IP string `header:"X-Original-Forwarded-For"`
UserAgent string `header:"User-Agent"` UserAgent string `header:"User-Agent"`
LoginType string `header:"Login-Type"`
CfToken string `json:"cf_token,optional"` CfToken string `json:"cf_token,optional"`
} }
// User login response // User login response
ResetPasswordRequest { ResetPasswordRequest {
Identifier string `json:"identifier"`
Email string `json:"email" validate:"required"` Email string `json:"email" validate:"required"`
Password string `json:"password" validate:"required"` Password string `json:"password" validate:"required"`
Code string `json:"code,optional"` Code string `json:"code,optional"`
IP string `header:"X-Original-Forwarded-For"` IP string `header:"X-Original-Forwarded-For"`
UserAgent string `header:"User-Agent"` UserAgent string `header:"User-Agent"`
LoginType string `header:"Login-Type"`
CfToken string `json:"cf_token,optional"` CfToken string `json:"cf_token,optional"`
} }
LoginResponse { LoginResponse {
@ -60,12 +66,14 @@ type (
} }
// login request // login request
TelephoneLoginRequest { TelephoneLoginRequest {
Identifier string `json:"identifier"`
Telephone string `json:"telephone" validate:"required"` Telephone string `json:"telephone" validate:"required"`
TelephoneCode string `json:"telephone_code"` TelephoneCode string `json:"telephone_code"`
TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` TelephoneAreaCode string `json:"telephone_area_code" validate:"required"`
Password string `json:"password"` Password string `json:"password"`
IP string `header:"X-Original-Forwarded-For"` IP string `header:"X-Original-Forwarded-For"`
UserAgent string `header:"User-Agent"` UserAgent string `header:"User-Agent"`
LoginType string `header:"Login-Type"`
CfToken string `json:"cf_token,optional"` CfToken string `json:"cf_token,optional"`
} }
// Check user is exist request // Check user is exist request
@ -79,6 +87,7 @@ type (
} }
// User login response // User login response
TelephoneRegisterRequest { TelephoneRegisterRequest {
Identifier string `json:"identifier"`
Telephone string `json:"telephone" validate:"required"` Telephone string `json:"telephone" validate:"required"`
TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` TelephoneAreaCode string `json:"telephone_area_code" validate:"required"`
Password string `json:"password" validate:"required"` Password string `json:"password" validate:"required"`
@ -86,16 +95,19 @@ type (
Code string `json:"code,optional"` Code string `json:"code,optional"`
IP string `header:"X-Original-Forwarded-For"` IP string `header:"X-Original-Forwarded-For"`
UserAgent string `header:"User-Agent"` UserAgent string `header:"User-Agent"`
LoginType string `header:"Login-Type,optional"`
CfToken string `json:"cf_token,optional"` CfToken string `json:"cf_token,optional"`
} }
// User login response // User login response
TelephoneResetPasswordRequest { TelephoneResetPasswordRequest {
Identifier string `json:"identifier"`
Telephone string `json:"telephone" validate:"required"` Telephone string `json:"telephone" validate:"required"`
TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` TelephoneAreaCode string `json:"telephone_area_code" validate:"required"`
Password string `json:"password" validate:"required"` Password string `json:"password" validate:"required"`
Code string `json:"code,optional"` Code string `json:"code,optional"`
IP string `header:"X-Original-Forwarded-For"` IP string `header:"X-Original-Forwarded-For"`
UserAgent string `header:"User-Agent"` UserAgent string `header:"User-Agent"`
LoginType string `header:"Login-Type,optional"`
CfToken string `json:"cf_token,optional"` CfToken string `json:"cf_token,optional"`
} }
AppleLoginCallbackRequest { AppleLoginCallbackRequest {
@ -107,11 +119,18 @@ type (
Code string `form:"code"` Code string `form:"code"`
State string `form:"state"` 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 ( @server (
prefix: v1/auth prefix: v1/auth
group: auth group: auth
middleware: DeviceMiddleware
) )
service ppanel { service ppanel {
@doc "User login" @doc "User login"
@ -145,6 +164,10 @@ service ppanel {
@doc "Reset password" @doc "Reset password"
@handler TelephoneResetPassword @handler TelephoneResetPassword
post /reset/telephone (TelephoneResetPasswordRequest) returns (LoginResponse) post /reset/telephone (TelephoneResetPasswordRequest) returns (LoginResponse)
@doc "Device Login"
@handler DeviceLogin
post /login/device (DeviceLoginRequest) returns (LoginResponse)
} }
@server ( @server (

View File

@ -87,11 +87,17 @@ type (
Total int64 `json:"total"` Total int64 `json:"total"`
List []SubscribeClient `json:"list"` List []SubscribeClient `json:"list"`
} }
HeartbeatResponse {
Status bool `json:"status"`
Message string `json:"message,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"`
}
) )
@server ( @server (
prefix: v1/common prefix: v1/common
group: common group: common
middleware: DeviceMiddleware
) )
service ppanel { service ppanel {
@doc "Get global config" @doc "Get global config"
@ -129,5 +135,9 @@ service ppanel {
@doc "Get Client" @doc "Get Client"
@handler GetClient @handler GetClient
get /client returns (GetSubscribeClientResponse) get /client returns (GetSubscribeClientResponse)
@doc "Heartbeat"
@handler Heartbeat
get /heartbeat returns (HeartbeatResponse)
} }

View File

@ -13,7 +13,7 @@ import "../types.api"
@server ( @server (
prefix: v1/public/announcement prefix: v1/public/announcement
group: public/announcement group: public/announcement
middleware: AuthMiddleware middleware: AuthMiddleware,DeviceMiddleware
) )
service ppanel { service ppanel {
@doc "Query announcement" @doc "Query announcement"

View File

@ -13,7 +13,7 @@ import "../types.api"
@server ( @server (
prefix: v1/public/document prefix: v1/public/document
group: public/document group: public/document
middleware: AuthMiddleware middleware: AuthMiddleware,DeviceMiddleware
) )
service ppanel { service ppanel {
@doc "Get document list" @doc "Get document list"

View File

@ -13,7 +13,7 @@ import "../types.api"
@server ( @server (
prefix: v1/public/order prefix: v1/public/order
group: public/order group: public/order
middleware: AuthMiddleware middleware: AuthMiddleware,DeviceMiddleware
) )
service ppanel { service ppanel {
@doc "Pre create order" @doc "Pre create order"

View File

@ -13,7 +13,7 @@ import "../types.api"
@server ( @server (
prefix: v1/public/payment prefix: v1/public/payment
group: public/payment group: public/payment
middleware: AuthMiddleware middleware: AuthMiddleware,DeviceMiddleware
) )
service ppanel { service ppanel {
@doc "Get available payment methods" @doc "Get available payment methods"

View File

@ -70,6 +70,7 @@ type (
@server ( @server (
prefix: v1/public/portal prefix: v1/public/portal
group: public/portal group: public/portal
middleware: DeviceMiddleware
) )
service ppanel { service ppanel {
@doc "Get available payment methods" @doc "Get available payment methods"

View File

@ -14,16 +14,54 @@ type (
QuerySubscribeListRequest { QuerySubscribeListRequest {
Language string `form:"language"` 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 ( @server (
prefix: v1/public/subscribe prefix: v1/public/subscribe
group: public/subscribe group: public/subscribe
middleware: AuthMiddleware middleware: AuthMiddleware,DeviceMiddleware
) )
service ppanel { service ppanel {
@doc "Get subscribe list" @doc "Get subscribe list"
@handler QuerySubscribeList @handler QuerySubscribeList
get /list (QuerySubscribeListRequest) returns (QuerySubscribeListResponse) get /list (QuerySubscribeListRequest) returns (QuerySubscribeListResponse)
@doc "Get user subscribe node info"
@handler QueryUserSubscribeNodeList
get /node/list returns (QueryUserSubscribeNodeListResponse)
} }

View File

@ -45,7 +45,7 @@ type (
@server ( @server (
prefix: v1/public/ticket prefix: v1/public/ticket
group: public/ticket group: public/ticket
middleware: AuthMiddleware middleware: AuthMiddleware,DeviceMiddleware
) )
service ppanel { service ppanel {
@doc "Get ticket list" @doc "Get ticket list"

View File

@ -97,12 +97,48 @@ type (
Email string `json:"email" validate:"required"` Email string `json:"email" validate:"required"`
Code string `json:"code" 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 ( @server (
prefix: v1/public/user prefix: v1/public/user
group: public/user group: public/user
middleware: AuthMiddleware middleware: AuthMiddleware,DeviceMiddleware
) )
service ppanel { service ppanel {
@doc "Query User Info" @doc "Query User Info"
@ -192,5 +228,29 @@ service ppanel {
@doc "Update Bind Email" @doc "Update Bind Email"
@handler UpdateBindEmail @handler UpdateBindEmail
put /bind_email (UpdateBindEmailRequest) 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)
} }

View File

@ -28,6 +28,7 @@ type (
EnableTradeNotify bool `json:"enable_trade_notify"` EnableTradeNotify bool `json:"enable_trade_notify"`
AuthMethods []UserAuthMethod `json:"auth_methods"` AuthMethods []UserAuthMethod `json:"auth_methods"`
UserDevices []UserDevice `json:"user_devices"` UserDevices []UserDevice `json:"user_devices"`
Rules []string `json:"rules"`
CreatedAt int64 `json:"created_at"` CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"` UpdatedAt int64 `json:"updated_at"`
DeletedAt int64 `json:"deleted_at,omitempty"` DeletedAt int64 `json:"deleted_at,omitempty"`
@ -115,6 +116,7 @@ type (
AuthConfig { AuthConfig {
Mobile MobileAuthenticateConfig `json:"mobile"` Mobile MobileAuthenticateConfig `json:"mobile"`
Email EmailAuthticateConfig `json:"email"` Email EmailAuthticateConfig `json:"email"`
Device DeviceAuthticateConfig `json:"device"`
Register PubilcRegisterConfig `json:"register"` Register PubilcRegisterConfig `json:"register"`
} }
PubilcRegisterConfig { PubilcRegisterConfig {
@ -134,6 +136,12 @@ type (
EnableDomainSuffix bool `json:"enable_domain_suffix"` EnableDomainSuffix bool `json:"enable_domain_suffix"`
DomainSuffixList string `json:"domain_suffix_list"` 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 { RegisterConfig {
StopRegister bool `json:"stop_register"` StopRegister bool `json:"stop_register"`
EnableTrial bool `json:"enable_trial"` EnableTrial bool `json:"enable_trial"`
@ -464,6 +472,7 @@ type (
Upload int64 `json:"upload"` Upload int64 `json:"upload"`
Token string `json:"token"` Token string `json:"token"`
Status uint8 `json:"status"` Status uint8 `json:"status"`
Short string `json:"short"`
CreatedAt int64 `json:"created_at"` CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"` UpdatedAt int64 `json:"updated_at"`
} }

42
cmd/migrate.go Normal file
View File

@ -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")
}

8
go.mod
View File

@ -44,7 +44,7 @@ require (
go.opentelemetry.io/otel/sdk v1.29.0 go.opentelemetry.io/otel/sdk v1.29.0
go.opentelemetry.io/otel/trace v1.29.0 go.opentelemetry.io/otel/trace v1.29.0
go.uber.org/zap v1.27.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/oauth2 v0.25.0
golang.org/x/time v0.6.0 golang.org/x/time v0.6.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
@ -60,6 +60,7 @@ require (
github.com/fatih/color v1.18.0 github.com/fatih/color v1.18.0
github.com/goccy/go-json v0.10.4 github.com/goccy/go-json v0.10.4
github.com/golang-migrate/migrate/v4 v4.18.2 github.com/golang-migrate/migrate/v4 v4.18.2
github.com/oschwald/geoip2-golang v1.13.0
github.com/spaolacci/murmur3 v1.1.0 github.com/spaolacci/murmur3 v1.1.0
google.golang.org/grpc v1.64.1 google.golang.org/grpc v1.64.1
google.golang.org/protobuf v1.36.3 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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/openzipkin/zipkin-go v0.4.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/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/robfig/cron/v3 v3.0.1 // 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/arch v0.13.0 // indirect
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d // indirect golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d // indirect
golang.org/x/net v0.34.0 // indirect golang.org/x/net v0.34.0 // indirect
golang.org/x/sys v0.29.0 // indirect golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.21.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/api v0.0.0-20240513163218-0867130af1f8 // indirect
google.golang.org/genproto/googleapis/rpc 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 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect

16
go.sum
View File

@ -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/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 h1:zjqfqHjUpPmB3c1GlCvvgsM1G4LkvqQbBDueDOCg/jA=
github.com/openzipkin/zipkin-go v0.4.2/go.mod h1:ZeVkFjuuBiSy13y8vpSDCjMi9GoI3hPpCJSBx/EYFhY= 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 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 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-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.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.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= 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-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 h1:N0hmiNbwsSNwHBAvR3QB5w25pUwH4tK0Y/RltD1j1h4=
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= 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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.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.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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-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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 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.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.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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 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.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 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=

View File

@ -9,6 +9,7 @@ import (
"net/http" "net/http"
"os" "os"
"github.com/perfect-panel/server/internal/report"
"github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/logger"
"gorm.io/driver/mysql" "gorm.io/driver/mysql"
@ -35,10 +36,30 @@ func Config(path string) (chan bool, *http.Server) {
configPath = path configPath = path
// Create a new Gin instance // Create a new Gin instance
r := gin.Default() 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 // Create a new HTTP server
server := &http.Server{ server := &http.Server{
Addr: ":8080", Addr: fmt.Sprintf("%s:%d", host, port),
Handler: r, Handler: r,
} }
// Load templates // Load templates

26
initialize/device.go Normal file
View File

@ -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
}

View File

@ -9,6 +9,7 @@ func StartInitSystemConfig(svc *svc.ServiceContext) {
Site(svc) Site(svc)
Node(svc) Node(svc)
Email(svc) Email(svc)
Device(svc)
Invite(svc) Invite(svc)
Verify(svc) Verify(svc)
Subscribe(svc) Subscribe(svc)

View File

@ -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;

View File

@ -0,0 +1,3 @@
ALTER TABLE `user`
DROP COLUMN `algo`,
DROP COLUMN `salt`;

View File

@ -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;

View File

@ -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'
);

View File

@ -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'
);

View File

@ -0,0 +1 @@
ALTER TABLE traffic_log DROP INDEX idx_timestamp;

View File

@ -0,0 +1 @@
ALTER TABLE traffic_log ADD INDEX idx_timestamp (timestamp);

View File

@ -0,0 +1,2 @@
ALTER TABLE `user_subscribe`
DROP COLUMN `note`;

View File

@ -0,0 +1,4 @@
ALTER TABLE `user_subscribe`
ADD COLUMN `note` VARCHAR(500) NOT NULL DEFAULT ''
COMMENT 'User note for subscription'
AFTER `status`;

View File

@ -0,0 +1,2 @@
ALTER TABLE `user`
DROP COLUMN IF EXISTS `rules`;

View File

@ -0,0 +1,4 @@
ALTER TABLE `user`
ADD COLUMN `rules` TEXT NULL
COMMENT 'User rules for subscription'
AFTER `created_at`;

View File

@ -0,0 +1,5 @@
DROP TABLE IF EXISTS `withdrawals`;
DELETE FROM `system`
WHERE `category` = 'invite'
AND `key` = 'WithdrawalMethod';

View File

@ -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');

View File

@ -2,6 +2,7 @@ package initialize
import ( import (
"errors" "errors"
"time"
"github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/model/user"
"gorm.io/gorm" "gorm.io/gorm"
@ -16,13 +17,17 @@ func Migrate(ctx *svc.ServiceContext) {
mc := orm.Mysql{ mc := orm.Mysql{
Config: ctx.Config.MySQL, Config: ctx.Config.MySQL,
} }
now := time.Now()
if err := migrate.Migrate(mc.Dsn()).Up(); err != nil { if err := migrate.Migrate(mc.Dsn()).Up(); err != nil {
if !errors.Is(err, migrate.NoChange) { if !errors.Is(err, migrate.NoChange) {
logger.Errorf("[Migrate] Up error: %v", err.Error()) logger.Errorf("[Migrate] Up error: %v", err.Error())
panic(err) panic(err)
} }
logger.Info("[Migrate] database not change") logger.Info("[Migrate] database not change")
} else {
logger.Info("[Migrate] Database change, took " + time.Since(now).String())
} }
// if not found admin user // if not found admin user
err := ctx.DB.Transaction(func(tx *gorm.DB) error { err := ctx.DB.Transaction(func(tx *gorm.DB) error {
var count int64 var count int64

View File

@ -2,6 +2,7 @@ package config
import ( import (
"encoding/json" "encoding/json"
"github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/orm" "github.com/perfect-panel/server/pkg/orm"
) )
@ -20,6 +21,7 @@ type Config struct {
Node NodeConfig `yaml:"Node"` Node NodeConfig `yaml:"Node"`
Mobile MobileConfig `yaml:"Mobile"` Mobile MobileConfig `yaml:"Mobile"`
Email EmailConfig `yaml:"Email"` Email EmailConfig `yaml:"Email"`
Device DeviceConfig `yaml:"device"`
Verify Verify `yaml:"Verify"` Verify Verify `yaml:"Verify"`
VerifyCode VerifyCode `yaml:"VerifyCode"` VerifyCode VerifyCode `yaml:"VerifyCode"`
Register RegisterConfig `yaml:"Register"` Register RegisterConfig `yaml:"Register"`
@ -95,6 +97,14 @@ type MobileConfig struct {
Whitelist []string `yaml:"whitelist"` 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 { type SiteConfig struct {
Host string `yaml:"Host" default:""` Host string `yaml:"Host" default:""`
SiteName string `yaml:"SiteName" default:""` SiteName string `yaml:"SiteName" default:""`
@ -194,8 +204,8 @@ type InviteConfig struct {
FirstPurchasePercentage int64 `yaml:"FirstPurchasePercentage" default:"20"` FirstPurchasePercentage int64 `yaml:"FirstPurchasePercentage" default:"20"`
FirstYearlyPurchasePercentage int64 `yaml:"FirstYearlyPurchasePercentage" default:"25"` FirstYearlyPurchasePercentage int64 `yaml:"FirstYearlyPurchasePercentage" default:"25"`
NonFirstPurchasePercentage int64 `yaml:"NonFirstPurchasePercentage" default:"10"` NonFirstPurchasePercentage int64 `yaml:"NonFirstPurchasePercentage" default:"10"`
OnlyFirstPurchase bool `yaml:"OnlyFirstPurchase" default:"true"` ReferralPercentage uint8 `yaml:"ReferralPercentage" default:"10"`
ReferralPercentage int64 `yaml:"ReferralPercentage" default:"20"` OnlyFirstPurchase bool `yaml:"OnlyFirstPurchase" default:"false"`
} }
type Telegram struct { type Telegram struct {

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -10,7 +10,6 @@ import (
// Get Client // Get Client
func GetClientHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { func GetClientHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) { return func(c *gin.Context) {
l := common.NewGetClientLogic(c.Request.Context(), svcCtx) l := common.NewGetClientLogic(c.Request.Context(), svcCtx)
resp, err := l.GetClient() resp, err := l.GetClient()
result.HttpResult(c, resp, err) result.HttpResult(c, resp, err)

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -385,6 +385,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
// Get subscribe list // Get subscribe list
adminSubscribeGroupRouter.GET("/list", adminSubscribe.GetSubscribeListHandler(serverCtx)) adminSubscribeGroupRouter.GET("/list", adminSubscribe.GetSubscribeListHandler(serverCtx))
// Reset all subscribe tokens
adminSubscribeGroupRouter.POST("/reset_all_token", adminSubscribe.ResetAllSubscribeTokenHandler(serverCtx))
// Subscribe sort // Subscribe sort
adminSubscribeGroupRouter.POST("/sort", adminSubscribe.SubscribeSortHandler(serverCtx)) adminSubscribeGroupRouter.POST("/sort", adminSubscribe.SubscribeSortHandler(serverCtx))
} }
@ -408,6 +411,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
// Update invite config // Update invite config
adminSystemGroupRouter.PUT("/invite_config", adminSystem.UpdateInviteConfigHandler(serverCtx)) adminSystemGroupRouter.PUT("/invite_config", adminSystem.UpdateInviteConfigHandler(serverCtx))
// Get Module Config
adminSystemGroupRouter.GET("/module", adminSystem.GetModuleConfigHandler(serverCtx))
// Get node config // Get node config
adminSystemGroupRouter.GET("/node_config", adminSystem.GetNodeConfigHandler(serverCtx)) adminSystemGroupRouter.GET("/node_config", adminSystem.GetNodeConfigHandler(serverCtx))
@ -487,6 +493,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
adminToolGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) adminToolGroupRouter.Use(middleware.AuthMiddleware(serverCtx))
{ {
// Query IP Location
adminToolGroupRouter.GET("/ip/location", adminTool.QueryIPLocationHandler(serverCtx))
// Get System Log // Get System Log
adminToolGroupRouter.GET("/log", adminTool.GetSystemLogHandler(serverCtx)) adminToolGroupRouter.GET("/log", adminTool.GetSystemLogHandler(serverCtx))
@ -578,6 +587,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
} }
authGroupRouter := router.Group("/v1/auth") authGroupRouter := router.Group("/v1/auth")
authGroupRouter.Use(middleware.DeviceMiddleware(serverCtx))
{ {
// Check user is exist // Check user is exist
@ -589,6 +599,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
// User login // User login
authGroupRouter.POST("/login", auth.UserLoginHandler(serverCtx)) authGroupRouter.POST("/login", auth.UserLoginHandler(serverCtx))
// Device Login
authGroupRouter.POST("/login/device", auth.DeviceLoginHandler(serverCtx))
// User Telephone login // User Telephone login
authGroupRouter.POST("/login/telephone", auth.TelephoneLoginHandler(serverCtx)) 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 := router.Group("/v1/common")
commonGroupRouter.Use(middleware.DeviceMiddleware(serverCtx))
{ {
// Get Ads // Get Ads
@ -630,6 +644,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
// Get Client // Get Client
commonGroupRouter.GET("/client", common.GetClientHandler(serverCtx)) commonGroupRouter.GET("/client", common.GetClientHandler(serverCtx))
// Heartbeat
commonGroupRouter.GET("/heartbeat", common.HeartbeatHandler(serverCtx))
// Get verification code // Get verification code
commonGroupRouter.POST("/send_code", common.SendEmailCodeHandler(serverCtx)) 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 := router.Group("/v1/public/announcement")
publicAnnouncementGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) publicAnnouncementGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
{ {
// Query announcement // Query announcement
@ -658,7 +675,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
} }
publicDocumentGroupRouter := router.Group("/v1/public/document") publicDocumentGroupRouter := router.Group("/v1/public/document")
publicDocumentGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) publicDocumentGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
{ {
// Get document detail // Get document detail
@ -669,7 +686,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
} }
publicOrderGroupRouter := router.Group("/v1/public/order") publicOrderGroupRouter := router.Group("/v1/public/order")
publicOrderGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) publicOrderGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
{ {
// Close order // Close order
@ -698,7 +715,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
} }
publicPaymentGroupRouter := router.Group("/v1/public/payment") publicPaymentGroupRouter := router.Group("/v1/public/payment")
publicPaymentGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) publicPaymentGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
{ {
// Get available payment methods // Get available payment methods
@ -706,6 +723,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
} }
publicPortalGroupRouter := router.Group("/v1/public/portal") publicPortalGroupRouter := router.Group("/v1/public/portal")
publicPortalGroupRouter.Use(middleware.DeviceMiddleware(serverCtx))
{ {
// Purchase Checkout // Purchase Checkout
@ -728,15 +746,18 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
} }
publicSubscribeGroupRouter := router.Group("/v1/public/subscribe") publicSubscribeGroupRouter := router.Group("/v1/public/subscribe")
publicSubscribeGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) publicSubscribeGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
{ {
// Get subscribe list // Get subscribe list
publicSubscribeGroupRouter.GET("/list", publicSubscribe.QuerySubscribeListHandler(serverCtx)) 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 := router.Group("/v1/public/ticket")
publicTicketGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) publicTicketGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
{ {
// Update ticket status // Update ticket status
@ -756,7 +777,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
} }
publicUserGroupRouter := router.Group("/v1/public/user") publicUserGroupRouter := router.Group("/v1/public/user")
publicUserGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) publicUserGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
{ {
// Query User Affiliate Count // Query User Affiliate Count
@ -786,6 +807,12 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
// Query User Commission Log // Query User Commission Log
publicUserGroupRouter.GET("/commission_log", publicUser.QueryUserCommissionLogHandler(serverCtx)) 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 // Query User Info
publicUserGroupRouter.GET("/info", publicUser.QueryUserInfoHandler(serverCtx)) publicUserGroupRouter.GET("/info", publicUser.QueryUserInfoHandler(serverCtx))
@ -801,15 +828,24 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
// Update User Password // Update User Password
publicUserGroupRouter.PUT("/password", publicUser.UpdateUserPasswordHandler(serverCtx)) publicUserGroupRouter.PUT("/password", publicUser.UpdateUserPasswordHandler(serverCtx))
// Update User Rules
publicUserGroupRouter.PUT("/rules", publicUser.UpdateUserRulesHandler(serverCtx))
// Query User Subscribe // Query User Subscribe
publicUserGroupRouter.GET("/subscribe", publicUser.QueryUserSubscribeHandler(serverCtx)) publicUserGroupRouter.GET("/subscribe", publicUser.QueryUserSubscribeHandler(serverCtx))
// Get Subscribe Log // Get Subscribe Log
publicUserGroupRouter.GET("/subscribe_log", publicUser.GetSubscribeLogHandler(serverCtx)) publicUserGroupRouter.GET("/subscribe_log", publicUser.GetSubscribeLogHandler(serverCtx))
// Update User Subscribe Note
publicUserGroupRouter.PUT("/subscribe_note", publicUser.UpdateUserSubscribeNoteHandler(serverCtx))
// Reset User Subscribe Token // Reset User Subscribe Token
publicUserGroupRouter.PUT("/subscribe_token", publicUser.ResetUserSubscribeTokenHandler(serverCtx)) publicUserGroupRouter.PUT("/subscribe_token", publicUser.ResetUserSubscribeTokenHandler(serverCtx))
// Unbind Device
publicUserGroupRouter.PUT("/unbind_device", publicUser.UnbindDeviceHandler(serverCtx))
// Unbind OAuth // Unbind OAuth
publicUserGroupRouter.POST("/unbind_oauth", publicUser.UnbindOAuthHandler(serverCtx)) publicUserGroupRouter.POST("/unbind_oauth", publicUser.UnbindOAuthHandler(serverCtx))
@ -824,6 +860,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
// Verify Email // Verify Email
publicUserGroupRouter.POST("/verify_email", publicUser.VerifyEmailHandler(serverCtx)) publicUserGroupRouter.POST("/verify_email", publicUser.VerifyEmailHandler(serverCtx))
// Query Withdrawal Log
publicUserGroupRouter.GET("/withdrawal_log", publicUser.QueryWithdrawalLogHandler(serverCtx))
} }
serverGroupRouter := router.Group("/v1/server") serverGroupRouter := router.Group("/v1/server")

View File

@ -36,6 +36,12 @@ func QueryServerProtocolConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Co
fmt.Printf("[QueryServerProtocolConfigHandler] - ShouldBindQuery request: %+v\n", req) 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) l := server.NewQueryServerProtocolConfigLogic(c.Request.Context(), svcCtx)
resp, err := l.QueryServerProtocolConfig(&req) resp, err := l.QueryServerProtocolConfig(&req)
result.HttpResult(c, resp, err) result.HttpResult(c, resp, err)

View File

@ -23,6 +23,22 @@ func SubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
ua := c.GetHeader("User-Agent") ua := c.GetHeader("User-Agent")
req.UA = c.Request.Header.Get("User-Agent") req.UA = c.Request.Header.Get("User-Agent")
req.Flag = c.Query("flag") 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 svcCtx.Config.Subscribe.UserAgentLimit {
if ua == "" { if ua == "" {

View File

@ -29,10 +29,12 @@ func NewPreviewSubscribeTemplateLogic(ctx context.Context, svcCtx *svc.ServiceCo
} }
func (l *PreviewSubscribeTemplateLogic) PreviewSubscribeTemplate(req *types.PreviewSubscribeTemplateRequest) (resp *types.PreviewSubscribeTemplateResponse, err error) { func (l *PreviewSubscribeTemplateLogic) PreviewSubscribeTemplate(req *types.PreviewSubscribeTemplateRequest) (resp *types.PreviewSubscribeTemplateResponse, err error) {
enable := true
_, servers, err := l.svcCtx.NodeModel.FilterNodeList(l.ctx, &node.FilterNodeParams{ _, servers, err := l.svcCtx.NodeModel.FilterNodeList(l.ctx, &node.FilterNodeParams{
Page: 1, Page: 1,
Size: 1000, Size: 1000,
Preload: true, Preload: true,
Enabled: &enable,
}) })
if err != nil { if err != nil {
l.Errorf("[PreviewSubscribeTemplateLogic] FindAllServer error: %v", err.Error()) l.Errorf("[PreviewSubscribeTemplateLogic] FindAllServer error: %v", err.Error())

View File

@ -92,6 +92,9 @@ func (l *UpdateAuthMethodConfigLogic) UpdateGlobal(method string) {
if method == "mobile" { if method == "mobile" {
initialize.Mobile(l.svcCtx) initialize.Mobile(l.svcCtx)
} }
if method == "device" {
initialize.Device(l.svcCtx)
}
} }
func validatePlatformConfig(platform string, cfg map[string]interface{}) (interface{}, error) { func validatePlatformConfig(platform string, cfg map[string]interface{}) (interface{}, error) {

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"github.com/perfect-panel/server/internal/report"
paymentPlatform "github.com/perfect-panel/server/pkg/payment" paymentPlatform "github.com/perfect-panel/server/pkg/payment"
"github.com/perfect-panel/server/internal/model/payment" "github.com/perfect-panel/server/internal/model/payment"
@ -43,15 +44,31 @@ func (l *GetPaymentMethodListLogic) GetPaymentMethodList(req *types.GetPaymentMe
Total: total, Total: total,
List: make([]types.PaymentMethodDetail, len(list)), List: make([]types.PaymentMethodDetail, len(list)),
} }
// gateway mod
isGatewayMod := report.IsGatewayMode()
for i, v := range list { for i, v := range list {
config := make(map[string]interface{}) config := make(map[string]interface{})
_ = json.Unmarshal([]byte(v.Config), &config) _ = json.Unmarshal([]byte(v.Config), &config)
notifyUrl := "" notifyUrl := ""
if paymentPlatform.ParsePlatform(v.Platform) != paymentPlatform.Balance { if paymentPlatform.ParsePlatform(v.Platform) != paymentPlatform.Balance {
notifyUrl = v.Domain
if 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 { } 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{ resp.List[i] = types.PaymentMethodDetail{

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -40,6 +40,7 @@ func (l *CreateUserLogic) CreateUser(req *types.CreateUserRequest) error {
pwd := tool.EncodePassWord(req.Password) pwd := tool.EncodePassWord(req.Password)
newUser := &user.User{ newUser := &user.User{
Password: pwd, Password: pwd,
Algo: "default",
ReferralPercentage: req.ReferralPercentage, ReferralPercentage: req.ReferralPercentage,
OnlyFirstPurchase: &req.OnlyFirstPurchase, OnlyFirstPurchase: &req.OnlyFirstPurchase,
ReferCode: req.ReferCode, ReferCode: req.ReferCode,

View File

@ -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") 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.Password = tool.EncodePassWord(req.Password)
userInfo.Algo = "default"
} }
err = l.svcCtx.UserModel.Update(l.ctx, userInfo) err = l.svcCtx.UserModel.Update(l.ctx, userInfo)

View File

@ -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
}

View File

@ -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
}

View File

@ -104,9 +104,26 @@ func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordRequest) (res
// Update password // Update password
userInfo.Password = tool.EncodePassWord(req.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()) 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 // Generate session id
sessionId := uuidx.NewUUID().String() sessionId := uuidx.NewUUID().String()
// Generate token // Generate token
@ -116,6 +133,7 @@ func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordRequest) (res
l.svcCtx.Config.JwtAuth.AccessExpire, l.svcCtx.Config.JwtAuth.AccessExpire,
jwt.WithOption("UserId", userInfo.Id), jwt.WithOption("UserId", userInfo.Id),
jwt.WithOption("SessionId", sessionId), jwt.WithOption("SessionId", sessionId),
jwt.WithOption("LoginType", req.LoginType),
) )
if err != nil { if err != nil {
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))

View File

@ -98,7 +98,7 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r
if req.TelephoneCode == "" { if req.TelephoneCode == "" {
// Verify password // 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") return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserPasswordError), "user password")
} }
} else { } else {
@ -124,6 +124,23 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r
l.svcCtx.Redis.Del(l.ctx, cacheKey) 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 // Generate session id
sessionId := uuidx.NewUUID().String() sessionId := uuidx.NewUUID().String()
// Generate token // Generate token
@ -133,6 +150,7 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r
l.svcCtx.Config.JwtAuth.AccessExpire, l.svcCtx.Config.JwtAuth.AccessExpire,
jwt.WithOption("UserId", userInfo.Id), jwt.WithOption("UserId", userInfo.Id),
jwt.WithOption("SessionId", sessionId), jwt.WithOption("SessionId", sessionId),
jwt.WithOption("LoginType", req.LoginType),
) )
if err != nil { if err != nil {
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))

View File

@ -78,11 +78,27 @@ func (l *TelephoneResetPasswordLogic) TelephoneResetPassword(req *types.Telephon
// Generate password // Generate password
pwd := tool.EncodePassWord(req.Password) pwd := tool.EncodePassWord(req.Password)
userInfo.Password = pwd userInfo.Password = pwd
userInfo.Algo = "default"
err = l.svcCtx.UserModel.Update(l.ctx, userInfo) err = l.svcCtx.UserModel.Update(l.ctx, userInfo)
if err != nil { if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "update user password failed: %v", err.Error()) 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 // Generate session id
sessionId := uuidx.NewUUID().String() sessionId := uuidx.NewUUID().String()
// Generate token // Generate token
@ -92,6 +108,7 @@ func (l *TelephoneResetPasswordLogic) TelephoneResetPassword(req *types.Telephon
l.svcCtx.Config.JwtAuth.AccessExpire, l.svcCtx.Config.JwtAuth.AccessExpire,
jwt.WithOption("UserId", userInfo.Id), jwt.WithOption("UserId", userInfo.Id),
jwt.WithOption("SessionId", sessionId), jwt.WithOption("SessionId", sessionId),
jwt.WithOption("LoginType", req.LoginType),
) )
if err != nil { if err != nil {
l.Errorw("[UserLogin] token generate error", logger.Field("error", err.Error())) l.Errorw("[UserLogin] token generate error", logger.Field("error", err.Error()))

View File

@ -107,6 +107,7 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR
pwd := tool.EncodePassWord(req.Password) pwd := tool.EncodePassWord(req.Password)
userInfo := &user.User{ userInfo := &user.User{
Password: pwd, Password: pwd,
Algo: "default",
OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase, OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase,
AuthMethods: []user.AuthMethods{ AuthMethods: []user.AuthMethods{
{ {
@ -138,6 +139,22 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR
} }
return nil 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 // Generate session id
sessionId := uuidx.NewUUID().String() sessionId := uuidx.NewUUID().String()
// Generate token // Generate token
@ -147,6 +164,7 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR
l.svcCtx.Config.JwtAuth.AccessExpire, l.svcCtx.Config.JwtAuth.AccessExpire,
jwt.WithOption("UserId", userInfo.Id), jwt.WithOption("UserId", userInfo.Id),
jwt.WithOption("SessionId", sessionId), jwt.WithOption("SessionId", sessionId),
jwt.WithOption("LoginType", req.LoginType),
) )
if err != nil { if err != nil {
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))

View File

@ -6,6 +6,7 @@ import (
"time" "time"
"github.com/perfect-panel/server/internal/model/log" "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/pkg/logger"
"github.com/perfect-panel/server/internal/config" "github.com/perfect-panel/server/internal/config"
@ -76,9 +77,25 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log
} }
// Verify password // 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") 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 // Generate session id
sessionId := uuidx.NewUUID().String() sessionId := uuidx.NewUUID().String()
// Generate token // Generate token
@ -88,6 +105,7 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log
l.svcCtx.Config.JwtAuth.AccessExpire, l.svcCtx.Config.JwtAuth.AccessExpire,
jwt.WithOption("UserId", userInfo.Id), jwt.WithOption("UserId", userInfo.Id),
jwt.WithOption("SessionId", sessionId), jwt.WithOption("SessionId", sessionId),
jwt.WithOption("LoginType", req.LoginType),
) )
if err != nil { if err != nil {
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))

View File

@ -90,6 +90,7 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
pwd := tool.EncodePassWord(req.Password) pwd := tool.EncodePassWord(req.Password)
userInfo := &user.User{ userInfo := &user.User{
Password: pwd, Password: pwd,
Algo: "default",
OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase, OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase,
} }
if referer != nil { if referer != nil {
@ -125,6 +126,21 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
} }
return nil 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 // Generate session id
sessionId := uuidx.NewUUID().String() sessionId := uuidx.NewUUID().String()
// Generate token // Generate token
@ -134,6 +150,7 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
l.svcCtx.Config.JwtAuth.AccessExpire, l.svcCtx.Config.JwtAuth.AccessExpire,
jwt.WithOption("UserId", userInfo.Id), jwt.WithOption("UserId", userInfo.Id),
jwt.WithOption("SessionId", sessionId), jwt.WithOption("SessionId", sessionId),
jwt.WithOption("LoginType", req.LoginType),
) )
if err != nil { if err != nil {
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))

View File

@ -2,6 +2,7 @@ package common
import ( import (
"context" "context"
"encoding/json"
"github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/internal/types"
@ -67,6 +68,10 @@ func (l *GetGlobalConfigLogic) GetGlobalConfig() (resp *types.GetGlobalConfigRes
for _, method := range authMethods { for _, method := range authMethods {
if *method.Enabled { if *method.Enabled {
methods = append(methods, method.Method) 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 resp.OAuthMethods = methods

View File

@ -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
}

View File

@ -57,7 +57,7 @@ func (l *EPayNotifyLogic) EPayNotify(req *types.EPayNotifyRequest) error {
return err return err
} }
// Verify sign // 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 { if !client.VerifySign(urlParamsToMap(l.ctx.Request.URL.RawQuery)) && !l.svcCtx.Config.Debug {
l.Logger.Error("[EPayNotify] Verify sign failed") l.Logger.Error("[EPayNotify] Verify sign failed")
return nil return nil

View File

@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/perfect-panel/server/internal/model/log" "github.com/perfect-panel/server/internal/model/log"
"github.com/perfect-panel/server/internal/report"
"github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/pkg/constant"
paymentPlatform "github.com/perfect-panel/server/pkg/payment" 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()) return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error())
} }
// Initialize EPay client with merchant credentials // 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 // Convert order amount to CNY using current exchange rate
amount, err := l.queryExchangeRate("CNY", info.Amount) amount, err := l.queryExchangeRate("CNY", info.Amount)
@ -275,16 +276,29 @@ func (l *PurchaseCheckoutLogic) epayPayment(config *payment.Payment, info *order
return "", err return "", err
} }
// gateway mod
isGatewayMod := report.IsGatewayMode()
// Build notification URL for payment status callbacks // Build notification URL for payment status callbacks
notifyUrl := "" notifyUrl := ""
if config.Domain != "" { 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 { } else {
host, ok := l.ctx.Value(constant.CtxKeyRequestHost).(string) host, ok := l.ctx.Value(constant.CtxKeyRequestHost).(string)
if !ok { if !ok {
host = l.svcCtx.Config.Host 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 // 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()) return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error())
} }
// Initialize EPay client with merchant credentials // 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 // Convert order amount to CNY using current exchange rate
amount, err := l.queryExchangeRate("CNY", info.Amount) amount, err := l.queryExchangeRate("CNY", info.Amount)
@ -317,18 +331,29 @@ func (l *PurchaseCheckoutLogic) CryptoSaaSPayment(config *payment.Payment, info
return "", err return "", err
} }
// gateway mod
isGatewayMod := report.IsGatewayMode()
// Build notification URL for payment status callbacks // Build notification URL for payment status callbacks
notifyUrl := "" notifyUrl := ""
if config.Domain != "" { 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 { } else {
host, ok := l.ctx.Value(constant.CtxKeyRequestHost).(string) host, ok := l.ctx.Value(constant.CtxKeyRequestHost).(string)
if !ok { if !ok {
host = l.svcCtx.Config.Host 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 // Create payment URL for user redirection
url := client.CreatePayUrl(epay.Order{ url := client.CreatePayUrl(epay.Order{
Name: l.svcCtx.Config.Site.SiteName, Name: l.svcCtx.Config.Site.SiteName,
@ -347,6 +372,11 @@ func (l *PurchaseCheckoutLogic) queryExchangeRate(to string, src int64) (amount
// Convert cents to decimal amount // Convert cents to decimal amount
amount = float64(src) / float64(100) amount = float64(src) / float64(100)
if l.svcCtx.ExchangeRate != 0 && to == "CNY" {
amount = amount * l.svcCtx.ExchangeRate
return amount, nil
}
// Retrieve system currency configuration // Retrieve system currency configuration
currency, err := l.svcCtx.SystemModel.GetCurrencyConfig(l.ctx) currency, err := l.svcCtx.SystemModel.GetCurrencyConfig(l.ctx)
if err != nil { if err != nil {

View File

@ -83,6 +83,12 @@ func (l *PurchaseLogic) Purchase(req *types.PortalPurchaseRequest) (resp *types.
if couponInfo.Count != 0 && couponInfo.Count <= couponInfo.UsedCount { if couponInfo.Count != 0 && couponInfo.Count <= couponInfo.UsedCount {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponInsufficientUsage), "coupon used") 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) couponSub := tool.StringToInt64Slice(couponInfo.Subscribe)
if len(couponSub) > 0 && !tool.Contains(couponSub, req.SubscribeId) { if len(couponSub) > 0 && !tool.Contains(couponSub, req.SubscribeId) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotApplicable), "coupon not match") return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotApplicable), "coupon not match")

View File

@ -59,8 +59,7 @@ func (l *QuerySubscribeListLogic) QuerySubscribeList(req *types.QuerySubscribeLi
list[i] = sub list[i] = sub
// 通过服务组查询关联的节点数量 // 通过服务组查询关联的节点数量
if item.ServerGroup != "" { if item.ServerGroup != "" {
groupIds := tool.StringToInt64Slice(item.ServerGroup) servers, err := l.svcCtx.ServerModel.FindServerListByGroupIds(l.ctx, tool.StringToInt64Slice(item.ServerGroup))
servers, err := l.svcCtx.ServerModel.FindServerListByGroupIds(l.ctx, groupIds)
if err != nil { if err != nil {
l.Errorw("[QuerySubscribeListLogic] FindServerListByGroupIds error", logger.Field("error", err.Error())) l.Errorw("[QuerySubscribeListLogic] FindServerListByGroupIds error", logger.Field("error", err.Error()))
sub.ServerCount = 0 sub.ServerCount = 0

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -60,25 +60,8 @@ func (l *QueryUserSubscribeLogic) QueryUserSubscribe() (resp *types.QueryUserSub
} }
} }
// 计算节点数量(通过服务组关联的实际节点数量) short, _ := tool.FixedUniqueString(item.Token, 8, "")
if item.Subscribe != nil { sub.Short = short
// 获取服务组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
}
sub.ResetTime = calculateNextResetTime(&sub) sub.ResetTime = calculateNextResetTime(&sub)
resp.List = append(resp.List, sub) resp.List = append(resp.List, sub)
} }

View File

@ -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
}

View File

@ -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
})
}

View File

@ -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
}

View File

@ -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
}

View File

@ -9,6 +9,8 @@ const (
AnyTLS = "anytls" AnyTLS = "anytls"
Tuic = "tuic" Tuic = "tuic"
Hysteria = "hysteria" Hysteria = "hysteria"
// Deprecated: Hysteria2 is deprecated, use Hysteria instead
// TODO: remove in future versions
Hysteria2 = "hysteria2" Hysteria2 = "hysteria2"
) )

View File

@ -57,13 +57,19 @@ func (l *GetServerConfigLogic) GetServerConfig(req *types.GetServerConfigRequest
return nil, err return nil, err
} }
// compatible hysteria2, remove in future versions
protocolRequest := req.Protocol
if protocolRequest == Hysteria2 {
protocolRequest = Hysteria
}
protocols, err := data.UnmarshalProtocols() protocols, err := data.UnmarshalProtocols()
if err != nil { if err != nil {
return nil, err return nil, err
} }
var cfg map[string]interface{} var cfg map[string]interface{}
for _, protocol := range protocols { for _, protocol := range protocols {
if protocol.Type == req.Protocol { if protocol.Type == protocolRequest {
cfg = l.compatible(protocol) cfg = l.compatible(protocol)
break break
} }
@ -209,7 +215,7 @@ func (l *GetServerConfigLogic) compatible(config node.Protocol) map[string]inter
RealityShortId: config.RealityShortId, RealityShortId: config.RealityShortId,
}, },
} }
case Hysteria2, Hysteria: case Hysteria:
result = Hysteria2Node{ result = Hysteria2Node{
Port: config.Port, Port: config.Port,
HopPorts: config.HopPorts, HopPorts: config.HopPorts,

View File

@ -249,8 +249,9 @@ func (l *SubscribeLogic) createExpiredServers() []*node.Node {
Port: 18080, Port: 18080,
Address: "127.0.0.1", Address: "127.0.0.1",
Server: &node.Server{ Server: &node.Server{
Id: 1,
Name: "Subscribe Expired", Name: "Subscribe Expired",
Protocols: "[{\"type:\"\"shadowsocks\",\"cipher\":\"aes-256-gcm\",\"port\":1}]", Protocols: "[{\"type\":\"shadowsocks\",\"cipher\":\"aes-256-gcm\",\"port\":1}]",
}, },
Protocol: "shadowsocks", Protocol: "shadowsocks",
Enabled: &enable, Enabled: &enable,
@ -261,8 +262,9 @@ func (l *SubscribeLogic) createExpiredServers() []*node.Node {
Port: 18080, Port: 18080,
Address: "127.0.0.1", Address: "127.0.0.1",
Server: &node.Server{ Server: &node.Server{
Id: 1,
Name: "Subscribe Expired", Name: "Subscribe Expired",
Protocols: "[{\"type:\"\"shadowsocks\",\"cipher\":\"aes-256-gcm\",\"port\":1}]", Protocols: "[{\"type\":\"shadowsocks\",\"cipher\":\"aes-256-gcm\",\"port\":1}]",
}, },
Protocol: "shadowsocks", Protocol: "shadowsocks",
Enabled: &enable, Enabled: &enable,

View File

@ -40,6 +40,11 @@ func AuthMiddleware(svc *svc.ServiceContext) func(c *gin.Context) {
c.Abort() c.Abort()
return return
} }
loginType := ""
if claims["LoginType"] != nil {
loginType = claims["LoginType"].(string)
}
// get user id from token // get user id from token
userId := int64(claims["UserId"].(float64)) userId := int64(claims["UserId"].(float64))
// get session id from token // get session id from token
@ -77,6 +82,7 @@ func AuthMiddleware(svc *svc.ServiceContext) func(c *gin.Context) {
c.Abort() c.Abort()
return return
} }
ctx = context.WithValue(ctx, constant.LoginType, loginType)
ctx = context.WithValue(ctx, constant.CtxKeyUser, userInfo) ctx = context.WithValue(ctx, constant.CtxKeyUser, userInfo)
ctx = context.WithValue(ctx, constant.CtxKeySessionID, sessionId) ctx = context.WithValue(ctx, constant.CtxKeySessionID, sessionId)
c.Request = c.Request.WithContext(ctx) c.Request = c.Request.WithContext(ctx)

View File

@ -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, &params)
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), &params)
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, &params)
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
}

View File

@ -43,7 +43,7 @@ func (m *customAnnouncementModel) GetAnnouncementListByPage(ctx context.Context,
if filter.Search != "" { if filter.Search != "" {
conn = conn.Where("`title` LIKE ? OR `content` LIKE ?", "%"+filter.Search+"%", "%"+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 return total, list, err
} }

View File

@ -46,8 +46,9 @@ const (
CommissionTypePurchase uint16 = 331 // Purchase CommissionTypePurchase uint16 = 331 // Purchase
CommissionTypeRenewal uint16 = 332 // Renewal CommissionTypeRenewal uint16 = 332 // Renewal
CommissionTypeRefund uint16 = 333 // Refund CommissionTypeRefund uint16 = 333 // Refund
commissionTypeWithdraw uint16 = 334 // withdraw CommissionTypeWithdraw uint16 = 334 // withdraw
CommissionTypeAdjust uint16 = 335 // Admin Adjust CommissionTypeAdjust uint16 = 335 // Admin Adjust
CommissionTypeConvertBalance uint16 = 336 // Convert to Balance
GiftTypeIncrease uint16 = 341 // Increase GiftTypeIncrease uint16 = 341 // Increase
GiftTypeReduce uint16 = 342 // Reduce GiftTypeReduce uint16 = 342 // Reduce
) )

View File

@ -23,6 +23,7 @@ type (
UpdateServer(ctx context.Context, data *Server, tx ...*gorm.DB) error UpdateServer(ctx context.Context, data *Server, tx ...*gorm.DB) error
DeleteServer(ctx context.Context, id int64, 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 Transaction(ctx context.Context, fn func(db *gorm.DB) error) error
QueryServerList(ctx context.Context, ids []int64) (servers []*Server, err error)
} }
NodeModel interface { NodeModel interface {

View File

@ -3,8 +3,10 @@ package node
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"github.com/perfect-panel/server/pkg/tool" "github.com/perfect-panel/server/pkg/tool"
"gorm.io/gorm"
) )
type customServerLogicModel interface { type customServerLogicModel interface {
@ -63,6 +65,12 @@ func (m *customServerModel) FilterServerList(ctx context.Context, params *Filter
return total, servers, err 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 // FilterNodeList Filter Node List
func (m *customServerModel) FilterNodeList(ctx context.Context, params *FilterNodeParams) (int64, []*Node, error) { func (m *customServerModel) FilterNodeList(ctx context.Context, params *FilterNodeParams) (int64, []*Node, error) {
var nodes []*Node var nodes []*Node
@ -85,10 +93,7 @@ func (m *customServerModel) FilterNodeList(ctx context.Context, params *FilterNo
query = query.Where("server_id IN ?", params.ServerId) query = query.Where("server_id IN ?", params.ServerId)
} }
if len(params.Tag) > 0 { if len(params.Tag) > 0 {
query = query.Where("1 = 0") query = query.Scopes(InSet("tags", params.Tag))
for _, tag := range params.Tag {
query = query.Or("FIND_IN_SET(?,tags)", tag)
}
} }
if params.Protocol != "" { if params.Protocol != "" {
query = query.Where("protocol = ?", params.Protocol) query = query.Where("protocol = ?", params.Protocol)
@ -165,3 +170,22 @@ func (m *customServerModel) ClearServerCache(ctx context.Context, serverId int64
} }
return nil 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...)
}
}

View File

@ -88,6 +88,7 @@ type EPayConfig struct {
Pid string `json:"pid"` Pid string `json:"pid"`
Url string `json:"url"` Url string `json:"url"`
Key string `json:"key"` Key string `json:"key"`
Type string `json:"type"`
} }
func (l *EPayConfig) Marshal() ([]byte, error) { func (l *EPayConfig) Marshal() ([]byte, error) {
@ -109,6 +110,7 @@ type CryptoSaaSConfig struct {
Endpoint string `json:"endpoint"` Endpoint string `json:"endpoint"`
AccountID string `json:"account_id"` AccountID string `json:"account_id"`
SecretKey string `json:"secret_key"` SecretKey string `json:"secret_key"`
Type string `json:"type"`
} }
func (l *CryptoSaaSConfig) Marshal() ([]byte, error) { func (l *CryptoSaaSConfig) Marshal() ([]byte, error) {

View File

@ -22,8 +22,7 @@ type Subscribe struct {
Quota int64 `gorm:"type:int;not null;default:0;comment:Quota"` Quota int64 `gorm:"type:int;not null;default:0;comment:Quota"`
Nodes string `gorm:"type:varchar(255);comment:Node Ids"` Nodes string `gorm:"type:varchar(255);comment:Node Ids"`
NodeTags string `gorm:"type:varchar(255);comment:Node Tags"` NodeTags string `gorm:"type:varchar(255);comment:Node Tags"`
ServerGroup string `gorm:"type:varchar(255);comment:Server Group Ids"` ServerGroup string `gorm:"type:varchar(255);comment:Server Group"`
Server string `gorm:"type:varchar(255);comment:Server Ids"`
Show *bool `gorm:"type:tinyint(1);not null;default:0;comment:Show portal page"` 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"` Sell *bool `gorm:"type:tinyint(1);not null;default:0;comment:Sell"`
Sort int64 `gorm:"type:int;not null;default:0;comment:Sort"` Sort int64 `gorm:"type:int;not null;default:0;comment:Sort"`

View File

@ -46,6 +46,16 @@ func (m *customUserModel) QueryDevicePageList(ctx context.Context, userId, subsc
return list, total, err 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 { func (m *customUserModel) UpdateDevice(ctx context.Context, data *Device, tx ...*gorm.DB) error {
old, err := m.FindOneDevice(ctx, data.Id) old, err := m.FindOneDevice(ctx, data.Id)
if err != nil { if err != nil {
@ -76,3 +86,18 @@ func (m *customUserModel) DeleteDevice(ctx context.Context, id int64, tx ...*gor
}, data.GetCacheKeys()...) }, data.GetCacheKeys()...)
return err 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
})
}

View File

@ -36,6 +36,7 @@ type SubscribeDetails struct {
Token string `gorm:"index:idx_token;unique;type:varchar(255);default:'';comment:Token"` 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"` 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"` 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"` CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
UpdatedAt time.Time `gorm:"comment:Update 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) FindUserAuthMethodByPlatform(ctx context.Context, userId int64, platform string) (*AuthMethods, error)
FindOneByEmail(ctx context.Context, email string) (*User, error) FindOneByEmail(ctx context.Context, email string) (*User, error)
FindOneDevice(ctx context.Context, id int64) (*Device, 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) QueryDevicePageList(ctx context.Context, userid, subscribeId int64, page, size int) ([]*Device, int64, error)
UpdateDevice(ctx context.Context, data *Device, tx ...*gorm.DB) error UpdateDevice(ctx context.Context, data *Device, tx ...*gorm.DB) error
FindOneDeviceByIdentifier(ctx context.Context, id string) (*Device, error) FindOneDeviceByIdentifier(ctx context.Context, id string) (*Device, error)
DeleteDevice(ctx context.Context, id int64, tx ...*gorm.DB) 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 ClearSubscribeCache(ctx context.Context, data ...*Subscribe) error
ClearUserCache(ctx context.Context, data ...*User) error ClearUserCache(ctx context.Context, data ...*User) error

View File

@ -7,6 +7,8 @@ import (
type User struct { type User struct {
Id int64 `gorm:"primaryKey"` Id int64 `gorm:"primaryKey"`
Password string `gorm:"type:varchar(100);not null;comment:User Password"` 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"` Avatar string `gorm:"type:MEDIUMTEXT;comment:User Avatar"`
Balance int64 `gorm:"default:0;comment:User Balance"` // User Balance Amount Balance int64 `gorm:"default:0;comment:User Balance"` // User Balance Amount
ReferCode string `gorm:"type:varchar(20);default:'';comment:Referral Code"` 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"` EnableTradeNotify *bool `gorm:"default:false;not null;comment:Enable Trade Notifications"`
AuthMethods []AuthMethods `gorm:"foreignKey:UserId;references:Id"` AuthMethods []AuthMethods `gorm:"foreignKey:UserId;references:Id"`
UserDevices []Device `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"` CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
UpdatedAt time.Time `gorm:"comment:Update 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"` 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"` 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"` 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"` CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"` UpdatedAt time.Time `gorm:"comment:Update Time"`
} }
@ -99,3 +103,18 @@ type DeviceOnlineRecord struct {
func (DeviceOnlineRecord) TableName() string { func (DeviceOnlineRecord) TableName() string {
return "user_device_online_record" 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"
}

15
internal/report/report.go Normal file
View File

@ -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"` // 响应数据
}

113
internal/report/tool.go Normal file
View File

@ -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
}

View File

@ -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)
}

Some files were not shown because too many files have changed in this diff Show More