Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
7229b41f
Unverified
Commit
7229b41f
authored
Feb 03, 2026
by
Wesley Liddick
Committed by
GitHub
Feb 03, 2026
Browse files
Merge pull request #420 from shuike/feat-invitation-code
feat: 增加邀请码注册功能
parents
a09478f3
0ed4a404
Changes
26
Hide whitespace changes
Inline
Side-by-side
backend/cmd/jwtgen/main.go
View file @
7229b41f
...
@@ -33,7 +33,7 @@ func main() {
...
@@ -33,7 +33,7 @@ func main() {
}()
}()
userRepo
:=
repository
.
NewUserRepository
(
client
,
sqlDB
)
userRepo
:=
repository
.
NewUserRepository
(
client
,
sqlDB
)
authService
:=
service
.
NewAuthService
(
userRepo
,
cfg
,
nil
,
nil
,
nil
,
nil
,
nil
)
authService
:=
service
.
NewAuthService
(
userRepo
,
nil
,
cfg
,
nil
,
nil
,
nil
,
nil
,
nil
)
ctx
,
cancel
:=
context
.
WithTimeout
(
context
.
Background
(),
5
*
time
.
Second
)
ctx
,
cancel
:=
context
.
WithTimeout
(
context
.
Background
(),
5
*
time
.
Second
)
defer
cancel
()
defer
cancel
()
...
...
backend/cmd/server/wire_gen.go
View file @
7229b41f
...
@@ -43,6 +43,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
...
@@ -43,6 +43,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
return
nil
,
err
return
nil
,
err
}
}
userRepository
:=
repository
.
NewUserRepository
(
client
,
db
)
userRepository
:=
repository
.
NewUserRepository
(
client
,
db
)
redeemCodeRepository
:=
repository
.
NewRedeemCodeRepository
(
client
)
settingRepository
:=
repository
.
NewSettingRepository
(
client
)
settingRepository
:=
repository
.
NewSettingRepository
(
client
)
settingService
:=
service
.
NewSettingService
(
settingRepository
,
configConfig
)
settingService
:=
service
.
NewSettingService
(
settingRepository
,
configConfig
)
redisClient
:=
repository
.
ProvideRedis
(
configConfig
)
redisClient
:=
repository
.
ProvideRedis
(
configConfig
)
...
@@ -61,24 +62,23 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
...
@@ -61,24 +62,23 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
apiKeyService
:=
service
.
NewAPIKeyService
(
apiKeyRepository
,
userRepository
,
groupRepository
,
userSubscriptionRepository
,
apiKeyCache
,
configConfig
)
apiKeyService
:=
service
.
NewAPIKeyService
(
apiKeyRepository
,
userRepository
,
groupRepository
,
userSubscriptionRepository
,
apiKeyCache
,
configConfig
)
apiKeyAuthCacheInvalidator
:=
service
.
ProvideAPIKeyAuthCacheInvalidator
(
apiKeyService
)
apiKeyAuthCacheInvalidator
:=
service
.
ProvideAPIKeyAuthCacheInvalidator
(
apiKeyService
)
promoService
:=
service
.
NewPromoService
(
promoCodeRepository
,
userRepository
,
billingCacheService
,
client
,
apiKeyAuthCacheInvalidator
)
promoService
:=
service
.
NewPromoService
(
promoCodeRepository
,
userRepository
,
billingCacheService
,
client
,
apiKeyAuthCacheInvalidator
)
authService
:=
service
.
NewAuthService
(
userRepository
,
configConfig
,
settingService
,
emailService
,
turnstileService
,
emailQueueService
,
promoService
)
authService
:=
service
.
NewAuthService
(
userRepository
,
redeemCodeRepository
,
configConfig
,
settingService
,
emailService
,
turnstileService
,
emailQueueService
,
promoService
)
userService
:=
service
.
NewUserService
(
userRepository
,
apiKeyAuthCacheInvalidator
)
userService
:=
service
.
NewUserService
(
userRepository
,
apiKeyAuthCacheInvalidator
)
subscriptionService
:=
service
.
NewSubscriptionService
(
groupRepository
,
userSubscriptionRepository
,
billingCacheService
)
redeemCache
:=
repository
.
NewRedeemCache
(
redisClient
)
redeemService
:=
service
.
NewRedeemService
(
redeemCodeRepository
,
userRepository
,
subscriptionService
,
redeemCache
,
billingCacheService
,
client
,
apiKeyAuthCacheInvalidator
)
secretEncryptor
,
err
:=
repository
.
NewAESEncryptor
(
configConfig
)
secretEncryptor
,
err
:=
repository
.
NewAESEncryptor
(
configConfig
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
err
return
nil
,
err
}
}
totpCache
:=
repository
.
NewTotpCache
(
redisClient
)
totpCache
:=
repository
.
NewTotpCache
(
redisClient
)
totpService
:=
service
.
NewTotpService
(
userRepository
,
secretEncryptor
,
totpCache
,
settingService
,
emailService
,
emailQueueService
)
totpService
:=
service
.
NewTotpService
(
userRepository
,
secretEncryptor
,
totpCache
,
settingService
,
emailService
,
emailQueueService
)
authHandler
:=
handler
.
NewAuthHandler
(
configConfig
,
authService
,
userService
,
settingService
,
promoService
,
totpService
)
authHandler
:=
handler
.
NewAuthHandler
(
configConfig
,
authService
,
userService
,
settingService
,
promoService
,
redeemService
,
totpService
)
userHandler
:=
handler
.
NewUserHandler
(
userService
)
userHandler
:=
handler
.
NewUserHandler
(
userService
)
apiKeyHandler
:=
handler
.
NewAPIKeyHandler
(
apiKeyService
)
apiKeyHandler
:=
handler
.
NewAPIKeyHandler
(
apiKeyService
)
usageLogRepository
:=
repository
.
NewUsageLogRepository
(
client
,
db
)
usageLogRepository
:=
repository
.
NewUsageLogRepository
(
client
,
db
)
usageService
:=
service
.
NewUsageService
(
usageLogRepository
,
userRepository
,
client
,
apiKeyAuthCacheInvalidator
)
usageService
:=
service
.
NewUsageService
(
usageLogRepository
,
userRepository
,
client
,
apiKeyAuthCacheInvalidator
)
usageHandler
:=
handler
.
NewUsageHandler
(
usageService
,
apiKeyService
)
usageHandler
:=
handler
.
NewUsageHandler
(
usageService
,
apiKeyService
)
redeemCodeRepository
:=
repository
.
NewRedeemCodeRepository
(
client
)
subscriptionService
:=
service
.
NewSubscriptionService
(
groupRepository
,
userSubscriptionRepository
,
billingCacheService
)
redeemCache
:=
repository
.
NewRedeemCache
(
redisClient
)
redeemService
:=
service
.
NewRedeemService
(
redeemCodeRepository
,
userRepository
,
subscriptionService
,
redeemCache
,
billingCacheService
,
client
,
apiKeyAuthCacheInvalidator
)
redeemHandler
:=
handler
.
NewRedeemHandler
(
redeemService
)
redeemHandler
:=
handler
.
NewRedeemHandler
(
redeemService
)
subscriptionHandler
:=
handler
.
NewSubscriptionHandler
(
subscriptionService
)
subscriptionHandler
:=
handler
.
NewSubscriptionHandler
(
subscriptionService
)
announcementRepository
:=
repository
.
NewAnnouncementRepository
(
client
)
announcementRepository
:=
repository
.
NewAnnouncementRepository
(
client
)
...
...
backend/internal/domain/constants.go
View file @
7229b41f
...
@@ -36,6 +36,7 @@ const (
...
@@ -36,6 +36,7 @@ const (
RedeemTypeBalance
=
"balance"
RedeemTypeBalance
=
"balance"
RedeemTypeConcurrency
=
"concurrency"
RedeemTypeConcurrency
=
"concurrency"
RedeemTypeSubscription
=
"subscription"
RedeemTypeSubscription
=
"subscription"
RedeemTypeInvitation
=
"invitation"
)
)
// PromoCode status constants
// PromoCode status constants
...
...
backend/internal/handler/admin/redeem_handler.go
View file @
7229b41f
...
@@ -29,7 +29,7 @@ func NewRedeemHandler(adminService service.AdminService) *RedeemHandler {
...
@@ -29,7 +29,7 @@ func NewRedeemHandler(adminService service.AdminService) *RedeemHandler {
// GenerateRedeemCodesRequest represents generate redeem codes request
// GenerateRedeemCodesRequest represents generate redeem codes request
type
GenerateRedeemCodesRequest
struct
{
type
GenerateRedeemCodesRequest
struct
{
Count
int
`json:"count" binding:"required,min=1,max=100"`
Count
int
`json:"count" binding:"required,min=1,max=100"`
Type
string
`json:"type" binding:"required,oneof=balance concurrency subscription"`
Type
string
`json:"type" binding:"required,oneof=balance concurrency subscription
invitation
"`
Value
float64
`json:"value" binding:"min=0"`
Value
float64
`json:"value" binding:"min=0"`
GroupID
*
int64
`json:"group_id"`
// 订阅类型必填
GroupID
*
int64
`json:"group_id"`
// 订阅类型必填
ValidityDays
int
`json:"validity_days" binding:"omitempty,max=36500"`
// 订阅类型使用,默认30天,最大100年
ValidityDays
int
`json:"validity_days" binding:"omitempty,max=36500"`
// 订阅类型使用,默认30天,最大100年
...
...
backend/internal/handler/admin/setting_handler.go
View file @
7229b41f
...
@@ -49,6 +49,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
...
@@ -49,6 +49,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
EmailVerifyEnabled
:
settings
.
EmailVerifyEnabled
,
EmailVerifyEnabled
:
settings
.
EmailVerifyEnabled
,
PromoCodeEnabled
:
settings
.
PromoCodeEnabled
,
PromoCodeEnabled
:
settings
.
PromoCodeEnabled
,
PasswordResetEnabled
:
settings
.
PasswordResetEnabled
,
PasswordResetEnabled
:
settings
.
PasswordResetEnabled
,
InvitationCodeEnabled
:
settings
.
InvitationCodeEnabled
,
TotpEnabled
:
settings
.
TotpEnabled
,
TotpEnabled
:
settings
.
TotpEnabled
,
TotpEncryptionKeyConfigured
:
h
.
settingService
.
IsTotpEncryptionKeyConfigured
(),
TotpEncryptionKeyConfigured
:
h
.
settingService
.
IsTotpEncryptionKeyConfigured
(),
SMTPHost
:
settings
.
SMTPHost
,
SMTPHost
:
settings
.
SMTPHost
,
...
@@ -94,11 +95,12 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
...
@@ -94,11 +95,12 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
// UpdateSettingsRequest 更新设置请求
// UpdateSettingsRequest 更新设置请求
type
UpdateSettingsRequest
struct
{
type
UpdateSettingsRequest
struct
{
// 注册设置
// 注册设置
RegistrationEnabled
bool
`json:"registration_enabled"`
RegistrationEnabled
bool
`json:"registration_enabled"`
EmailVerifyEnabled
bool
`json:"email_verify_enabled"`
EmailVerifyEnabled
bool
`json:"email_verify_enabled"`
PromoCodeEnabled
bool
`json:"promo_code_enabled"`
PromoCodeEnabled
bool
`json:"promo_code_enabled"`
PasswordResetEnabled
bool
`json:"password_reset_enabled"`
PasswordResetEnabled
bool
`json:"password_reset_enabled"`
TotpEnabled
bool
`json:"totp_enabled"`
// TOTP 双因素认证
InvitationCodeEnabled
bool
`json:"invitation_code_enabled"`
TotpEnabled
bool
`json:"totp_enabled"`
// TOTP 双因素认证
// 邮件服务设置
// 邮件服务设置
SMTPHost
string
`json:"smtp_host"`
SMTPHost
string
`json:"smtp_host"`
...
@@ -291,6 +293,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
...
@@ -291,6 +293,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
EmailVerifyEnabled
:
req
.
EmailVerifyEnabled
,
EmailVerifyEnabled
:
req
.
EmailVerifyEnabled
,
PromoCodeEnabled
:
req
.
PromoCodeEnabled
,
PromoCodeEnabled
:
req
.
PromoCodeEnabled
,
PasswordResetEnabled
:
req
.
PasswordResetEnabled
,
PasswordResetEnabled
:
req
.
PasswordResetEnabled
,
InvitationCodeEnabled
:
req
.
InvitationCodeEnabled
,
TotpEnabled
:
req
.
TotpEnabled
,
TotpEnabled
:
req
.
TotpEnabled
,
SMTPHost
:
req
.
SMTPHost
,
SMTPHost
:
req
.
SMTPHost
,
SMTPPort
:
req
.
SMTPPort
,
SMTPPort
:
req
.
SMTPPort
,
...
@@ -370,6 +373,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
...
@@ -370,6 +373,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
EmailVerifyEnabled
:
updatedSettings
.
EmailVerifyEnabled
,
EmailVerifyEnabled
:
updatedSettings
.
EmailVerifyEnabled
,
PromoCodeEnabled
:
updatedSettings
.
PromoCodeEnabled
,
PromoCodeEnabled
:
updatedSettings
.
PromoCodeEnabled
,
PasswordResetEnabled
:
updatedSettings
.
PasswordResetEnabled
,
PasswordResetEnabled
:
updatedSettings
.
PasswordResetEnabled
,
InvitationCodeEnabled
:
updatedSettings
.
InvitationCodeEnabled
,
TotpEnabled
:
updatedSettings
.
TotpEnabled
,
TotpEnabled
:
updatedSettings
.
TotpEnabled
,
TotpEncryptionKeyConfigured
:
h
.
settingService
.
IsTotpEncryptionKeyConfigured
(),
TotpEncryptionKeyConfigured
:
h
.
settingService
.
IsTotpEncryptionKeyConfigured
(),
SMTPHost
:
updatedSettings
.
SMTPHost
,
SMTPHost
:
updatedSettings
.
SMTPHost
,
...
...
backend/internal/handler/auth_handler.go
View file @
7229b41f
...
@@ -15,23 +15,25 @@ import (
...
@@ -15,23 +15,25 @@ import (
// AuthHandler handles authentication-related requests
// AuthHandler handles authentication-related requests
type
AuthHandler
struct
{
type
AuthHandler
struct
{
cfg
*
config
.
Config
cfg
*
config
.
Config
authService
*
service
.
AuthService
authService
*
service
.
AuthService
userService
*
service
.
UserService
userService
*
service
.
UserService
settingSvc
*
service
.
SettingService
settingSvc
*
service
.
SettingService
promoService
*
service
.
PromoService
promoService
*
service
.
PromoService
totpService
*
service
.
TotpService
redeemService
*
service
.
RedeemService
totpService
*
service
.
TotpService
}
}
// NewAuthHandler creates a new AuthHandler
// NewAuthHandler creates a new AuthHandler
func
NewAuthHandler
(
cfg
*
config
.
Config
,
authService
*
service
.
AuthService
,
userService
*
service
.
UserService
,
settingService
*
service
.
SettingService
,
promoService
*
service
.
PromoService
,
totpService
*
service
.
TotpService
)
*
AuthHandler
{
func
NewAuthHandler
(
cfg
*
config
.
Config
,
authService
*
service
.
AuthService
,
userService
*
service
.
UserService
,
settingService
*
service
.
SettingService
,
promoService
*
service
.
PromoService
,
redeemService
*
service
.
RedeemService
,
totpService
*
service
.
TotpService
)
*
AuthHandler
{
return
&
AuthHandler
{
return
&
AuthHandler
{
cfg
:
cfg
,
cfg
:
cfg
,
authService
:
authService
,
authService
:
authService
,
userService
:
userService
,
userService
:
userService
,
settingSvc
:
settingService
,
settingSvc
:
settingService
,
promoService
:
promoService
,
promoService
:
promoService
,
totpService
:
totpService
,
redeemService
:
redeemService
,
totpService
:
totpService
,
}
}
}
}
...
@@ -41,7 +43,8 @@ type RegisterRequest struct {
...
@@ -41,7 +43,8 @@ type RegisterRequest struct {
Password
string
`json:"password" binding:"required,min=6"`
Password
string
`json:"password" binding:"required,min=6"`
VerifyCode
string
`json:"verify_code"`
VerifyCode
string
`json:"verify_code"`
TurnstileToken
string
`json:"turnstile_token"`
TurnstileToken
string
`json:"turnstile_token"`
PromoCode
string
`json:"promo_code"`
// 注册优惠码
PromoCode
string
`json:"promo_code"`
// 注册优惠码
InvitationCode
string
`json:"invitation_code"`
// 邀请码
}
}
// SendVerifyCodeRequest 发送验证码请求
// SendVerifyCodeRequest 发送验证码请求
...
@@ -87,7 +90,7 @@ func (h *AuthHandler) Register(c *gin.Context) {
...
@@ -87,7 +90,7 @@ func (h *AuthHandler) Register(c *gin.Context) {
}
}
}
}
token
,
user
,
err
:=
h
.
authService
.
RegisterWithVerification
(
c
.
Request
.
Context
(),
req
.
Email
,
req
.
Password
,
req
.
VerifyCode
,
req
.
PromoCode
)
token
,
user
,
err
:=
h
.
authService
.
RegisterWithVerification
(
c
.
Request
.
Context
(),
req
.
Email
,
req
.
Password
,
req
.
VerifyCode
,
req
.
PromoCode
,
req
.
InvitationCode
)
if
err
!=
nil
{
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
response
.
ErrorFrom
(
c
,
err
)
return
return
...
@@ -346,6 +349,67 @@ func (h *AuthHandler) ValidatePromoCode(c *gin.Context) {
...
@@ -346,6 +349,67 @@ func (h *AuthHandler) ValidatePromoCode(c *gin.Context) {
})
})
}
}
// ValidateInvitationCodeRequest 验证邀请码请求
type
ValidateInvitationCodeRequest
struct
{
Code
string
`json:"code" binding:"required"`
}
// ValidateInvitationCodeResponse 验证邀请码响应
type
ValidateInvitationCodeResponse
struct
{
Valid
bool
`json:"valid"`
ErrorCode
string
`json:"error_code,omitempty"`
}
// ValidateInvitationCode 验证邀请码(公开接口,注册前调用)
// POST /api/v1/auth/validate-invitation-code
func
(
h
*
AuthHandler
)
ValidateInvitationCode
(
c
*
gin
.
Context
)
{
// 检查邀请码功能是否启用
if
h
.
settingSvc
==
nil
||
!
h
.
settingSvc
.
IsInvitationCodeEnabled
(
c
.
Request
.
Context
())
{
response
.
Success
(
c
,
ValidateInvitationCodeResponse
{
Valid
:
false
,
ErrorCode
:
"INVITATION_CODE_DISABLED"
,
})
return
}
var
req
ValidateInvitationCodeRequest
if
err
:=
c
.
ShouldBindJSON
(
&
req
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid request: "
+
err
.
Error
())
return
}
// 验证邀请码
redeemCode
,
err
:=
h
.
redeemService
.
GetByCode
(
c
.
Request
.
Context
(),
req
.
Code
)
if
err
!=
nil
{
response
.
Success
(
c
,
ValidateInvitationCodeResponse
{
Valid
:
false
,
ErrorCode
:
"INVITATION_CODE_NOT_FOUND"
,
})
return
}
// 检查类型和状态
if
redeemCode
.
Type
!=
service
.
RedeemTypeInvitation
{
response
.
Success
(
c
,
ValidateInvitationCodeResponse
{
Valid
:
false
,
ErrorCode
:
"INVITATION_CODE_INVALID"
,
})
return
}
if
redeemCode
.
Status
!=
service
.
StatusUnused
{
response
.
Success
(
c
,
ValidateInvitationCodeResponse
{
Valid
:
false
,
ErrorCode
:
"INVITATION_CODE_USED"
,
})
return
}
response
.
Success
(
c
,
ValidateInvitationCodeResponse
{
Valid
:
true
,
})
}
// ForgotPasswordRequest 忘记密码请求
// ForgotPasswordRequest 忘记密码请求
type
ForgotPasswordRequest
struct
{
type
ForgotPasswordRequest
struct
{
Email
string
`json:"email" binding:"required,email"`
Email
string
`json:"email" binding:"required,email"`
...
...
backend/internal/handler/dto/settings.go
View file @
7229b41f
...
@@ -6,6 +6,7 @@ type SystemSettings struct {
...
@@ -6,6 +6,7 @@ type SystemSettings struct {
EmailVerifyEnabled
bool
`json:"email_verify_enabled"`
EmailVerifyEnabled
bool
`json:"email_verify_enabled"`
PromoCodeEnabled
bool
`json:"promo_code_enabled"`
PromoCodeEnabled
bool
`json:"promo_code_enabled"`
PasswordResetEnabled
bool
`json:"password_reset_enabled"`
PasswordResetEnabled
bool
`json:"password_reset_enabled"`
InvitationCodeEnabled
bool
`json:"invitation_code_enabled"`
TotpEnabled
bool
`json:"totp_enabled"`
// TOTP 双因素认证
TotpEnabled
bool
`json:"totp_enabled"`
// TOTP 双因素认证
TotpEncryptionKeyConfigured
bool
`json:"totp_encryption_key_configured"`
// TOTP 加密密钥是否已配置
TotpEncryptionKeyConfigured
bool
`json:"totp_encryption_key_configured"`
// TOTP 加密密钥是否已配置
...
@@ -63,6 +64,7 @@ type PublicSettings struct {
...
@@ -63,6 +64,7 @@ type PublicSettings struct {
EmailVerifyEnabled
bool
`json:"email_verify_enabled"`
EmailVerifyEnabled
bool
`json:"email_verify_enabled"`
PromoCodeEnabled
bool
`json:"promo_code_enabled"`
PromoCodeEnabled
bool
`json:"promo_code_enabled"`
PasswordResetEnabled
bool
`json:"password_reset_enabled"`
PasswordResetEnabled
bool
`json:"password_reset_enabled"`
InvitationCodeEnabled
bool
`json:"invitation_code_enabled"`
TotpEnabled
bool
`json:"totp_enabled"`
// TOTP 双因素认证
TotpEnabled
bool
`json:"totp_enabled"`
// TOTP 双因素认证
TurnstileEnabled
bool
`json:"turnstile_enabled"`
TurnstileEnabled
bool
`json:"turnstile_enabled"`
TurnstileSiteKey
string
`json:"turnstile_site_key"`
TurnstileSiteKey
string
`json:"turnstile_site_key"`
...
...
backend/internal/handler/setting_handler.go
View file @
7229b41f
...
@@ -36,6 +36,7 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) {
...
@@ -36,6 +36,7 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) {
EmailVerifyEnabled
:
settings
.
EmailVerifyEnabled
,
EmailVerifyEnabled
:
settings
.
EmailVerifyEnabled
,
PromoCodeEnabled
:
settings
.
PromoCodeEnabled
,
PromoCodeEnabled
:
settings
.
PromoCodeEnabled
,
PasswordResetEnabled
:
settings
.
PasswordResetEnabled
,
PasswordResetEnabled
:
settings
.
PasswordResetEnabled
,
InvitationCodeEnabled
:
settings
.
InvitationCodeEnabled
,
TotpEnabled
:
settings
.
TotpEnabled
,
TotpEnabled
:
settings
.
TotpEnabled
,
TurnstileEnabled
:
settings
.
TurnstileEnabled
,
TurnstileEnabled
:
settings
.
TurnstileEnabled
,
TurnstileSiteKey
:
settings
.
TurnstileSiteKey
,
TurnstileSiteKey
:
settings
.
TurnstileSiteKey
,
...
...
backend/internal/server/api_contract_test.go
View file @
7229b41f
...
@@ -488,6 +488,7 @@ func TestAPIContracts(t *testing.T) {
...
@@ -488,6 +488,7 @@ func TestAPIContracts(t *testing.T) {
"fallback_model_openai": "gpt-4o",
"fallback_model_openai": "gpt-4o",
"enable_identity_patch": true,
"enable_identity_patch": true,
"identity_patch_prompt": "",
"identity_patch_prompt": "",
"invitation_code_enabled": false,
"home_content": "",
"home_content": "",
"hide_ccs_import_button": false,
"hide_ccs_import_button": false,
"purchase_subscription_enabled": false,
"purchase_subscription_enabled": false,
...
@@ -600,7 +601,7 @@ func newContractDeps(t *testing.T) *contractDeps {
...
@@ -600,7 +601,7 @@ func newContractDeps(t *testing.T) *contractDeps {
settingService
:=
service
.
NewSettingService
(
settingRepo
,
cfg
)
settingService
:=
service
.
NewSettingService
(
settingRepo
,
cfg
)
adminService
:=
service
.
NewAdminService
(
userRepo
,
groupRepo
,
&
accountRepo
,
proxyRepo
,
apiKeyRepo
,
redeemRepo
,
nil
,
nil
,
nil
,
nil
)
adminService
:=
service
.
NewAdminService
(
userRepo
,
groupRepo
,
&
accountRepo
,
proxyRepo
,
apiKeyRepo
,
redeemRepo
,
nil
,
nil
,
nil
,
nil
)
authHandler
:=
handler
.
NewAuthHandler
(
cfg
,
nil
,
userService
,
settingService
,
nil
,
nil
)
authHandler
:=
handler
.
NewAuthHandler
(
cfg
,
nil
,
userService
,
settingService
,
nil
,
nil
,
nil
)
apiKeyHandler
:=
handler
.
NewAPIKeyHandler
(
apiKeyService
)
apiKeyHandler
:=
handler
.
NewAPIKeyHandler
(
apiKeyService
)
usageHandler
:=
handler
.
NewUsageHandler
(
usageService
,
apiKeyService
)
usageHandler
:=
handler
.
NewUsageHandler
(
usageService
,
apiKeyService
)
adminSettingHandler
:=
adminhandler
.
NewSettingHandler
(
settingService
,
nil
,
nil
,
nil
)
adminSettingHandler
:=
adminhandler
.
NewSettingHandler
(
settingService
,
nil
,
nil
,
nil
)
...
...
backend/internal/server/routes/auth.go
View file @
7229b41f
...
@@ -32,6 +32,10 @@ func RegisterAuthRoutes(
...
@@ -32,6 +32,10 @@ func RegisterAuthRoutes(
auth
.
POST
(
"/validate-promo-code"
,
rateLimiter
.
LimitWithOptions
(
"validate-promo"
,
10
,
time
.
Minute
,
middleware
.
RateLimitOptions
{
auth
.
POST
(
"/validate-promo-code"
,
rateLimiter
.
LimitWithOptions
(
"validate-promo"
,
10
,
time
.
Minute
,
middleware
.
RateLimitOptions
{
FailureMode
:
middleware
.
RateLimitFailClose
,
FailureMode
:
middleware
.
RateLimitFailClose
,
}),
h
.
Auth
.
ValidatePromoCode
)
}),
h
.
Auth
.
ValidatePromoCode
)
// 邀请码验证接口添加速率限制:每分钟最多 10 次(Redis 故障时 fail-close)
auth
.
POST
(
"/validate-invitation-code"
,
rateLimiter
.
LimitWithOptions
(
"validate-invitation"
,
10
,
time
.
Minute
,
middleware
.
RateLimitOptions
{
FailureMode
:
middleware
.
RateLimitFailClose
,
}),
h
.
Auth
.
ValidateInvitationCode
)
// 忘记密码接口添加速率限制:每分钟最多 5 次(Redis 故障时 fail-close)
// 忘记密码接口添加速率限制:每分钟最多 5 次(Redis 故障时 fail-close)
auth
.
POST
(
"/forgot-password"
,
rateLimiter
.
LimitWithOptions
(
"forgot-password"
,
5
,
time
.
Minute
,
middleware
.
RateLimitOptions
{
auth
.
POST
(
"/forgot-password"
,
rateLimiter
.
LimitWithOptions
(
"forgot-password"
,
5
,
time
.
Minute
,
middleware
.
RateLimitOptions
{
FailureMode
:
middleware
.
RateLimitFailClose
,
FailureMode
:
middleware
.
RateLimitFailClose
,
...
...
backend/internal/service/auth_service.go
View file @
7229b41f
...
@@ -19,17 +19,19 @@ import (
...
@@ -19,17 +19,19 @@ import (
)
)
var
(
var
(
ErrInvalidCredentials
=
infraerrors
.
Unauthorized
(
"INVALID_CREDENTIALS"
,
"invalid email or password"
)
ErrInvalidCredentials
=
infraerrors
.
Unauthorized
(
"INVALID_CREDENTIALS"
,
"invalid email or password"
)
ErrUserNotActive
=
infraerrors
.
Forbidden
(
"USER_NOT_ACTIVE"
,
"user is not active"
)
ErrUserNotActive
=
infraerrors
.
Forbidden
(
"USER_NOT_ACTIVE"
,
"user is not active"
)
ErrEmailExists
=
infraerrors
.
Conflict
(
"EMAIL_EXISTS"
,
"email already exists"
)
ErrEmailExists
=
infraerrors
.
Conflict
(
"EMAIL_EXISTS"
,
"email already exists"
)
ErrEmailReserved
=
infraerrors
.
BadRequest
(
"EMAIL_RESERVED"
,
"email is reserved"
)
ErrEmailReserved
=
infraerrors
.
BadRequest
(
"EMAIL_RESERVED"
,
"email is reserved"
)
ErrInvalidToken
=
infraerrors
.
Unauthorized
(
"INVALID_TOKEN"
,
"invalid token"
)
ErrInvalidToken
=
infraerrors
.
Unauthorized
(
"INVALID_TOKEN"
,
"invalid token"
)
ErrTokenExpired
=
infraerrors
.
Unauthorized
(
"TOKEN_EXPIRED"
,
"token has expired"
)
ErrTokenExpired
=
infraerrors
.
Unauthorized
(
"TOKEN_EXPIRED"
,
"token has expired"
)
ErrTokenTooLarge
=
infraerrors
.
BadRequest
(
"TOKEN_TOO_LARGE"
,
"token too large"
)
ErrTokenTooLarge
=
infraerrors
.
BadRequest
(
"TOKEN_TOO_LARGE"
,
"token too large"
)
ErrTokenRevoked
=
infraerrors
.
Unauthorized
(
"TOKEN_REVOKED"
,
"token has been revoked"
)
ErrTokenRevoked
=
infraerrors
.
Unauthorized
(
"TOKEN_REVOKED"
,
"token has been revoked"
)
ErrEmailVerifyRequired
=
infraerrors
.
BadRequest
(
"EMAIL_VERIFY_REQUIRED"
,
"email verification is required"
)
ErrEmailVerifyRequired
=
infraerrors
.
BadRequest
(
"EMAIL_VERIFY_REQUIRED"
,
"email verification is required"
)
ErrRegDisabled
=
infraerrors
.
Forbidden
(
"REGISTRATION_DISABLED"
,
"registration is currently disabled"
)
ErrRegDisabled
=
infraerrors
.
Forbidden
(
"REGISTRATION_DISABLED"
,
"registration is currently disabled"
)
ErrServiceUnavailable
=
infraerrors
.
ServiceUnavailable
(
"SERVICE_UNAVAILABLE"
,
"service temporarily unavailable"
)
ErrServiceUnavailable
=
infraerrors
.
ServiceUnavailable
(
"SERVICE_UNAVAILABLE"
,
"service temporarily unavailable"
)
ErrInvitationCodeRequired
=
infraerrors
.
BadRequest
(
"INVITATION_CODE_REQUIRED"
,
"invitation code is required"
)
ErrInvitationCodeInvalid
=
infraerrors
.
BadRequest
(
"INVITATION_CODE_INVALID"
,
"invalid or used invitation code"
)
)
)
// maxTokenLength 限制 token 大小,避免超长 header 触发解析时的异常内存分配。
// maxTokenLength 限制 token 大小,避免超长 header 触发解析时的异常内存分配。
...
@@ -47,6 +49,7 @@ type JWTClaims struct {
...
@@ -47,6 +49,7 @@ type JWTClaims struct {
// AuthService 认证服务
// AuthService 认证服务
type
AuthService
struct
{
type
AuthService
struct
{
userRepo
UserRepository
userRepo
UserRepository
redeemRepo
RedeemCodeRepository
cfg
*
config
.
Config
cfg
*
config
.
Config
settingService
*
SettingService
settingService
*
SettingService
emailService
*
EmailService
emailService
*
EmailService
...
@@ -58,6 +61,7 @@ type AuthService struct {
...
@@ -58,6 +61,7 @@ type AuthService struct {
// NewAuthService 创建认证服务实例
// NewAuthService 创建认证服务实例
func
NewAuthService
(
func
NewAuthService
(
userRepo
UserRepository
,
userRepo
UserRepository
,
redeemRepo
RedeemCodeRepository
,
cfg
*
config
.
Config
,
cfg
*
config
.
Config
,
settingService
*
SettingService
,
settingService
*
SettingService
,
emailService
*
EmailService
,
emailService
*
EmailService
,
...
@@ -67,6 +71,7 @@ func NewAuthService(
...
@@ -67,6 +71,7 @@ func NewAuthService(
)
*
AuthService
{
)
*
AuthService
{
return
&
AuthService
{
return
&
AuthService
{
userRepo
:
userRepo
,
userRepo
:
userRepo
,
redeemRepo
:
redeemRepo
,
cfg
:
cfg
,
cfg
:
cfg
,
settingService
:
settingService
,
settingService
:
settingService
,
emailService
:
emailService
,
emailService
:
emailService
,
...
@@ -78,11 +83,11 @@ func NewAuthService(
...
@@ -78,11 +83,11 @@ func NewAuthService(
// Register 用户注册,返回token和用户
// Register 用户注册,返回token和用户
func
(
s
*
AuthService
)
Register
(
ctx
context
.
Context
,
email
,
password
string
)
(
string
,
*
User
,
error
)
{
func
(
s
*
AuthService
)
Register
(
ctx
context
.
Context
,
email
,
password
string
)
(
string
,
*
User
,
error
)
{
return
s
.
RegisterWithVerification
(
ctx
,
email
,
password
,
""
,
""
)
return
s
.
RegisterWithVerification
(
ctx
,
email
,
password
,
""
,
""
,
""
)
}
}
// RegisterWithVerification 用户注册(支持邮件验证
和
优惠码),返回token和用户
// RegisterWithVerification 用户注册(支持邮件验证
、
优惠码
和邀请码
),返回token和用户
func
(
s
*
AuthService
)
RegisterWithVerification
(
ctx
context
.
Context
,
email
,
password
,
verifyCode
,
promoCode
string
)
(
string
,
*
User
,
error
)
{
func
(
s
*
AuthService
)
RegisterWithVerification
(
ctx
context
.
Context
,
email
,
password
,
verifyCode
,
promoCode
,
invitationCode
string
)
(
string
,
*
User
,
error
)
{
// 检查是否开放注册(默认关闭:settingService 未配置时不允许注册)
// 检查是否开放注册(默认关闭:settingService 未配置时不允许注册)
if
s
.
settingService
==
nil
||
!
s
.
settingService
.
IsRegistrationEnabled
(
ctx
)
{
if
s
.
settingService
==
nil
||
!
s
.
settingService
.
IsRegistrationEnabled
(
ctx
)
{
return
""
,
nil
,
ErrRegDisabled
return
""
,
nil
,
ErrRegDisabled
...
@@ -93,6 +98,26 @@ func (s *AuthService) RegisterWithVerification(ctx context.Context, email, passw
...
@@ -93,6 +98,26 @@ func (s *AuthService) RegisterWithVerification(ctx context.Context, email, passw
return
""
,
nil
,
ErrEmailReserved
return
""
,
nil
,
ErrEmailReserved
}
}
// 检查是否需要邀请码
var
invitationRedeemCode
*
RedeemCode
if
s
.
settingService
!=
nil
&&
s
.
settingService
.
IsInvitationCodeEnabled
(
ctx
)
{
if
invitationCode
==
""
{
return
""
,
nil
,
ErrInvitationCodeRequired
}
// 验证邀请码
redeemCode
,
err
:=
s
.
redeemRepo
.
GetByCode
(
ctx
,
invitationCode
)
if
err
!=
nil
{
log
.
Printf
(
"[Auth] Invalid invitation code: %s, error: %v"
,
invitationCode
,
err
)
return
""
,
nil
,
ErrInvitationCodeInvalid
}
// 检查类型和状态
if
redeemCode
.
Type
!=
RedeemTypeInvitation
||
redeemCode
.
Status
!=
StatusUnused
{
log
.
Printf
(
"[Auth] Invitation code invalid: type=%s, status=%s"
,
redeemCode
.
Type
,
redeemCode
.
Status
)
return
""
,
nil
,
ErrInvitationCodeInvalid
}
invitationRedeemCode
=
redeemCode
}
// 检查是否需要邮件验证
// 检查是否需要邮件验证
if
s
.
settingService
!=
nil
&&
s
.
settingService
.
IsEmailVerifyEnabled
(
ctx
)
{
if
s
.
settingService
!=
nil
&&
s
.
settingService
.
IsEmailVerifyEnabled
(
ctx
)
{
// 如果邮件验证已开启但邮件服务未配置,拒绝注册
// 如果邮件验证已开启但邮件服务未配置,拒绝注册
...
@@ -153,6 +178,14 @@ func (s *AuthService) RegisterWithVerification(ctx context.Context, email, passw
...
@@ -153,6 +178,14 @@ func (s *AuthService) RegisterWithVerification(ctx context.Context, email, passw
return
""
,
nil
,
ErrServiceUnavailable
return
""
,
nil
,
ErrServiceUnavailable
}
}
// 标记邀请码为已使用(如果使用了邀请码)
if
invitationRedeemCode
!=
nil
{
if
err
:=
s
.
redeemRepo
.
Use
(
ctx
,
invitationRedeemCode
.
ID
,
user
.
ID
);
err
!=
nil
{
// 邀请码标记失败不影响注册,只记录日志
log
.
Printf
(
"[Auth] Failed to mark invitation code as used for user %d: %v"
,
user
.
ID
,
err
)
}
}
// 应用优惠码(如果提供且功能已启用)
// 应用优惠码(如果提供且功能已启用)
if
promoCode
!=
""
&&
s
.
promoService
!=
nil
&&
s
.
settingService
!=
nil
&&
s
.
settingService
.
IsPromoCodeEnabled
(
ctx
)
{
if
promoCode
!=
""
&&
s
.
promoService
!=
nil
&&
s
.
settingService
!=
nil
&&
s
.
settingService
.
IsPromoCodeEnabled
(
ctx
)
{
if
err
:=
s
.
promoService
.
ApplyPromoCode
(
ctx
,
user
.
ID
,
promoCode
);
err
!=
nil
{
if
err
:=
s
.
promoService
.
ApplyPromoCode
(
ctx
,
user
.
ID
,
promoCode
);
err
!=
nil
{
...
...
backend/internal/service/auth_service_register_test.go
View file @
7229b41f
...
@@ -115,6 +115,7 @@ func newAuthService(repo *userRepoStub, settings map[string]string, emailCache E
...
@@ -115,6 +115,7 @@ func newAuthService(repo *userRepoStub, settings map[string]string, emailCache E
return
NewAuthService
(
return
NewAuthService
(
repo
,
repo
,
nil
,
// redeemRepo
cfg
,
cfg
,
settingService
,
settingService
,
emailService
,
emailService
,
...
@@ -152,7 +153,7 @@ func TestAuthService_Register_EmailVerifyEnabledButServiceNotConfigured(t *testi
...
@@ -152,7 +153,7 @@ func TestAuthService_Register_EmailVerifyEnabledButServiceNotConfigured(t *testi
},
nil
)
},
nil
)
// 应返回服务不可用错误,而不是允许绕过验证
// 应返回服务不可用错误,而不是允许绕过验证
_
,
_
,
err
:=
service
.
RegisterWithVerification
(
context
.
Background
(),
"user@test.com"
,
"password"
,
"any-code"
,
""
)
_
,
_
,
err
:=
service
.
RegisterWithVerification
(
context
.
Background
(),
"user@test.com"
,
"password"
,
"any-code"
,
""
,
""
)
require
.
ErrorIs
(
t
,
err
,
ErrServiceUnavailable
)
require
.
ErrorIs
(
t
,
err
,
ErrServiceUnavailable
)
}
}
...
@@ -164,7 +165,7 @@ func TestAuthService_Register_EmailVerifyRequired(t *testing.T) {
...
@@ -164,7 +165,7 @@ func TestAuthService_Register_EmailVerifyRequired(t *testing.T) {
SettingKeyEmailVerifyEnabled
:
"true"
,
SettingKeyEmailVerifyEnabled
:
"true"
,
},
cache
)
},
cache
)
_
,
_
,
err
:=
service
.
RegisterWithVerification
(
context
.
Background
(),
"user@test.com"
,
"password"
,
""
,
""
)
_
,
_
,
err
:=
service
.
RegisterWithVerification
(
context
.
Background
(),
"user@test.com"
,
"password"
,
""
,
""
,
""
)
require
.
ErrorIs
(
t
,
err
,
ErrEmailVerifyRequired
)
require
.
ErrorIs
(
t
,
err
,
ErrEmailVerifyRequired
)
}
}
...
@@ -178,7 +179,7 @@ func TestAuthService_Register_EmailVerifyInvalid(t *testing.T) {
...
@@ -178,7 +179,7 @@ func TestAuthService_Register_EmailVerifyInvalid(t *testing.T) {
SettingKeyEmailVerifyEnabled
:
"true"
,
SettingKeyEmailVerifyEnabled
:
"true"
,
},
cache
)
},
cache
)
_
,
_
,
err
:=
service
.
RegisterWithVerification
(
context
.
Background
(),
"user@test.com"
,
"password"
,
"wrong"
,
""
)
_
,
_
,
err
:=
service
.
RegisterWithVerification
(
context
.
Background
(),
"user@test.com"
,
"password"
,
"wrong"
,
""
,
""
)
require
.
ErrorIs
(
t
,
err
,
ErrInvalidVerifyCode
)
require
.
ErrorIs
(
t
,
err
,
ErrInvalidVerifyCode
)
require
.
ErrorContains
(
t
,
err
,
"verify code"
)
require
.
ErrorContains
(
t
,
err
,
"verify code"
)
}
}
...
...
backend/internal/service/domain_constants.go
View file @
7229b41f
...
@@ -38,6 +38,7 @@ const (
...
@@ -38,6 +38,7 @@ const (
RedeemTypeBalance
=
domain
.
RedeemTypeBalance
RedeemTypeBalance
=
domain
.
RedeemTypeBalance
RedeemTypeConcurrency
=
domain
.
RedeemTypeConcurrency
RedeemTypeConcurrency
=
domain
.
RedeemTypeConcurrency
RedeemTypeSubscription
=
domain
.
RedeemTypeSubscription
RedeemTypeSubscription
=
domain
.
RedeemTypeSubscription
RedeemTypeInvitation
=
domain
.
RedeemTypeInvitation
)
)
// PromoCode status constants
// PromoCode status constants
...
@@ -71,10 +72,11 @@ const LinuxDoConnectSyntheticEmailDomain = "@linuxdo-connect.invalid"
...
@@ -71,10 +72,11 @@ const LinuxDoConnectSyntheticEmailDomain = "@linuxdo-connect.invalid"
// Setting keys
// Setting keys
const
(
const
(
// 注册设置
// 注册设置
SettingKeyRegistrationEnabled
=
"registration_enabled"
// 是否开放注册
SettingKeyRegistrationEnabled
=
"registration_enabled"
// 是否开放注册
SettingKeyEmailVerifyEnabled
=
"email_verify_enabled"
// 是否开启邮件验证
SettingKeyEmailVerifyEnabled
=
"email_verify_enabled"
// 是否开启邮件验证
SettingKeyPromoCodeEnabled
=
"promo_code_enabled"
// 是否启用优惠码功能
SettingKeyPromoCodeEnabled
=
"promo_code_enabled"
// 是否启用优惠码功能
SettingKeyPasswordResetEnabled
=
"password_reset_enabled"
// 是否启用忘记密码功能(需要先开启邮件验证)
SettingKeyPasswordResetEnabled
=
"password_reset_enabled"
// 是否启用忘记密码功能(需要先开启邮件验证)
SettingKeyInvitationCodeEnabled
=
"invitation_code_enabled"
// 是否启用邀请码注册
// 邮件服务设置
// 邮件服务设置
SettingKeySMTPHost
=
"smtp_host"
// SMTP服务器地址
SettingKeySMTPHost
=
"smtp_host"
// SMTP服务器地址
...
...
backend/internal/service/redeem_service.go
View file @
7229b41f
...
@@ -126,7 +126,8 @@ func (s *RedeemService) GenerateCodes(ctx context.Context, req GenerateCodesRequ
...
@@ -126,7 +126,8 @@ func (s *RedeemService) GenerateCodes(ctx context.Context, req GenerateCodesRequ
return
nil
,
errors
.
New
(
"count must be greater than 0"
)
return
nil
,
errors
.
New
(
"count must be greater than 0"
)
}
}
if
req
.
Value
<=
0
{
// 邀请码类型不需要数值,其他类型需要
if
req
.
Type
!=
RedeemTypeInvitation
&&
req
.
Value
<=
0
{
return
nil
,
errors
.
New
(
"value must be greater than 0"
)
return
nil
,
errors
.
New
(
"value must be greater than 0"
)
}
}
...
@@ -139,6 +140,12 @@ func (s *RedeemService) GenerateCodes(ctx context.Context, req GenerateCodesRequ
...
@@ -139,6 +140,12 @@ func (s *RedeemService) GenerateCodes(ctx context.Context, req GenerateCodesRequ
codeType
=
RedeemTypeBalance
codeType
=
RedeemTypeBalance
}
}
// 邀请码类型的 value 设为 0
value
:=
req
.
Value
if
codeType
==
RedeemTypeInvitation
{
value
=
0
}
codes
:=
make
([]
RedeemCode
,
0
,
req
.
Count
)
codes
:=
make
([]
RedeemCode
,
0
,
req
.
Count
)
for
i
:=
0
;
i
<
req
.
Count
;
i
++
{
for
i
:=
0
;
i
<
req
.
Count
;
i
++
{
code
,
err
:=
s
.
GenerateRandomCode
()
code
,
err
:=
s
.
GenerateRandomCode
()
...
@@ -149,7 +156,7 @@ func (s *RedeemService) GenerateCodes(ctx context.Context, req GenerateCodesRequ
...
@@ -149,7 +156,7 @@ func (s *RedeemService) GenerateCodes(ctx context.Context, req GenerateCodesRequ
codes
=
append
(
codes
,
RedeemCode
{
codes
=
append
(
codes
,
RedeemCode
{
Code
:
code
,
Code
:
code
,
Type
:
codeType
,
Type
:
codeType
,
Value
:
req
.
V
alue
,
Value
:
v
alue
,
Status
:
StatusUnused
,
Status
:
StatusUnused
,
})
})
}
}
...
...
backend/internal/service/setting_service.go
View file @
7229b41f
...
@@ -62,6 +62,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
...
@@ -62,6 +62,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
SettingKeyEmailVerifyEnabled
,
SettingKeyEmailVerifyEnabled
,
SettingKeyPromoCodeEnabled
,
SettingKeyPromoCodeEnabled
,
SettingKeyPasswordResetEnabled
,
SettingKeyPasswordResetEnabled
,
SettingKeyInvitationCodeEnabled
,
SettingKeyTotpEnabled
,
SettingKeyTotpEnabled
,
SettingKeyTurnstileEnabled
,
SettingKeyTurnstileEnabled
,
SettingKeyTurnstileSiteKey
,
SettingKeyTurnstileSiteKey
,
...
@@ -99,6 +100,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
...
@@ -99,6 +100,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
EmailVerifyEnabled
:
emailVerifyEnabled
,
EmailVerifyEnabled
:
emailVerifyEnabled
,
PromoCodeEnabled
:
settings
[
SettingKeyPromoCodeEnabled
]
!=
"false"
,
// 默认启用
PromoCodeEnabled
:
settings
[
SettingKeyPromoCodeEnabled
]
!=
"false"
,
// 默认启用
PasswordResetEnabled
:
passwordResetEnabled
,
PasswordResetEnabled
:
passwordResetEnabled
,
InvitationCodeEnabled
:
settings
[
SettingKeyInvitationCodeEnabled
]
==
"true"
,
TotpEnabled
:
settings
[
SettingKeyTotpEnabled
]
==
"true"
,
TotpEnabled
:
settings
[
SettingKeyTotpEnabled
]
==
"true"
,
TurnstileEnabled
:
settings
[
SettingKeyTurnstileEnabled
]
==
"true"
,
TurnstileEnabled
:
settings
[
SettingKeyTurnstileEnabled
]
==
"true"
,
TurnstileSiteKey
:
settings
[
SettingKeyTurnstileSiteKey
],
TurnstileSiteKey
:
settings
[
SettingKeyTurnstileSiteKey
],
...
@@ -141,6 +143,7 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
...
@@ -141,6 +143,7 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
EmailVerifyEnabled
bool
`json:"email_verify_enabled"`
EmailVerifyEnabled
bool
`json:"email_verify_enabled"`
PromoCodeEnabled
bool
`json:"promo_code_enabled"`
PromoCodeEnabled
bool
`json:"promo_code_enabled"`
PasswordResetEnabled
bool
`json:"password_reset_enabled"`
PasswordResetEnabled
bool
`json:"password_reset_enabled"`
InvitationCodeEnabled
bool
`json:"invitation_code_enabled"`
TotpEnabled
bool
`json:"totp_enabled"`
TotpEnabled
bool
`json:"totp_enabled"`
TurnstileEnabled
bool
`json:"turnstile_enabled"`
TurnstileEnabled
bool
`json:"turnstile_enabled"`
TurnstileSiteKey
string
`json:"turnstile_site_key,omitempty"`
TurnstileSiteKey
string
`json:"turnstile_site_key,omitempty"`
...
@@ -161,6 +164,7 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
...
@@ -161,6 +164,7 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
EmailVerifyEnabled
:
settings
.
EmailVerifyEnabled
,
EmailVerifyEnabled
:
settings
.
EmailVerifyEnabled
,
PromoCodeEnabled
:
settings
.
PromoCodeEnabled
,
PromoCodeEnabled
:
settings
.
PromoCodeEnabled
,
PasswordResetEnabled
:
settings
.
PasswordResetEnabled
,
PasswordResetEnabled
:
settings
.
PasswordResetEnabled
,
InvitationCodeEnabled
:
settings
.
InvitationCodeEnabled
,
TotpEnabled
:
settings
.
TotpEnabled
,
TotpEnabled
:
settings
.
TotpEnabled
,
TurnstileEnabled
:
settings
.
TurnstileEnabled
,
TurnstileEnabled
:
settings
.
TurnstileEnabled
,
TurnstileSiteKey
:
settings
.
TurnstileSiteKey
,
TurnstileSiteKey
:
settings
.
TurnstileSiteKey
,
...
@@ -188,6 +192,7 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet
...
@@ -188,6 +192,7 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet
updates
[
SettingKeyEmailVerifyEnabled
]
=
strconv
.
FormatBool
(
settings
.
EmailVerifyEnabled
)
updates
[
SettingKeyEmailVerifyEnabled
]
=
strconv
.
FormatBool
(
settings
.
EmailVerifyEnabled
)
updates
[
SettingKeyPromoCodeEnabled
]
=
strconv
.
FormatBool
(
settings
.
PromoCodeEnabled
)
updates
[
SettingKeyPromoCodeEnabled
]
=
strconv
.
FormatBool
(
settings
.
PromoCodeEnabled
)
updates
[
SettingKeyPasswordResetEnabled
]
=
strconv
.
FormatBool
(
settings
.
PasswordResetEnabled
)
updates
[
SettingKeyPasswordResetEnabled
]
=
strconv
.
FormatBool
(
settings
.
PasswordResetEnabled
)
updates
[
SettingKeyInvitationCodeEnabled
]
=
strconv
.
FormatBool
(
settings
.
InvitationCodeEnabled
)
updates
[
SettingKeyTotpEnabled
]
=
strconv
.
FormatBool
(
settings
.
TotpEnabled
)
updates
[
SettingKeyTotpEnabled
]
=
strconv
.
FormatBool
(
settings
.
TotpEnabled
)
// 邮件服务设置(只有非空才更新密码)
// 邮件服务设置(只有非空才更新密码)
...
@@ -286,6 +291,15 @@ func (s *SettingService) IsPromoCodeEnabled(ctx context.Context) bool {
...
@@ -286,6 +291,15 @@ func (s *SettingService) IsPromoCodeEnabled(ctx context.Context) bool {
return
value
!=
"false"
return
value
!=
"false"
}
}
// IsInvitationCodeEnabled 检查是否启用邀请码注册功能
func
(
s
*
SettingService
)
IsInvitationCodeEnabled
(
ctx
context
.
Context
)
bool
{
value
,
err
:=
s
.
settingRepo
.
GetValue
(
ctx
,
SettingKeyInvitationCodeEnabled
)
if
err
!=
nil
{
return
false
// 默认关闭
}
return
value
==
"true"
}
// IsPasswordResetEnabled 检查是否启用密码重置功能
// IsPasswordResetEnabled 检查是否启用密码重置功能
// 要求:必须同时开启邮件验证
// 要求:必须同时开启邮件验证
func
(
s
*
SettingService
)
IsPasswordResetEnabled
(
ctx
context
.
Context
)
bool
{
func
(
s
*
SettingService
)
IsPasswordResetEnabled
(
ctx
context
.
Context
)
bool
{
...
@@ -401,6 +415,7 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
...
@@ -401,6 +415,7 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
EmailVerifyEnabled
:
emailVerifyEnabled
,
EmailVerifyEnabled
:
emailVerifyEnabled
,
PromoCodeEnabled
:
settings
[
SettingKeyPromoCodeEnabled
]
!=
"false"
,
// 默认启用
PromoCodeEnabled
:
settings
[
SettingKeyPromoCodeEnabled
]
!=
"false"
,
// 默认启用
PasswordResetEnabled
:
emailVerifyEnabled
&&
settings
[
SettingKeyPasswordResetEnabled
]
==
"true"
,
PasswordResetEnabled
:
emailVerifyEnabled
&&
settings
[
SettingKeyPasswordResetEnabled
]
==
"true"
,
InvitationCodeEnabled
:
settings
[
SettingKeyInvitationCodeEnabled
]
==
"true"
,
TotpEnabled
:
settings
[
SettingKeyTotpEnabled
]
==
"true"
,
TotpEnabled
:
settings
[
SettingKeyTotpEnabled
]
==
"true"
,
SMTPHost
:
settings
[
SettingKeySMTPHost
],
SMTPHost
:
settings
[
SettingKeySMTPHost
],
SMTPUsername
:
settings
[
SettingKeySMTPUsername
],
SMTPUsername
:
settings
[
SettingKeySMTPUsername
],
...
...
backend/internal/service/settings_view.go
View file @
7229b41f
package
service
package
service
type
SystemSettings
struct
{
type
SystemSettings
struct
{
RegistrationEnabled
bool
RegistrationEnabled
bool
EmailVerifyEnabled
bool
EmailVerifyEnabled
bool
PromoCodeEnabled
bool
PromoCodeEnabled
bool
PasswordResetEnabled
bool
PasswordResetEnabled
bool
TotpEnabled
bool
// TOTP 双因素认证
InvitationCodeEnabled
bool
TotpEnabled
bool
// TOTP 双因素认证
SMTPHost
string
SMTPHost
string
SMTPPort
int
SMTPPort
int
...
@@ -61,21 +62,22 @@ type SystemSettings struct {
...
@@ -61,21 +62,22 @@ type SystemSettings struct {
}
}
type
PublicSettings
struct
{
type
PublicSettings
struct
{
RegistrationEnabled
bool
RegistrationEnabled
bool
EmailVerifyEnabled
bool
EmailVerifyEnabled
bool
PromoCodeEnabled
bool
PromoCodeEnabled
bool
PasswordResetEnabled
bool
PasswordResetEnabled
bool
TotpEnabled
bool
// TOTP 双因素认证
InvitationCodeEnabled
bool
TurnstileEnabled
bool
TotpEnabled
bool
// TOTP 双因素认证
TurnstileSiteKey
string
TurnstileEnabled
bool
SiteName
string
TurnstileSiteKey
string
SiteLogo
string
SiteName
string
SiteSubtitle
string
SiteLogo
string
APIBaseURL
string
SiteSubtitle
string
ContactInfo
string
APIBaseURL
string
DocURL
string
ContactInfo
string
HomeContent
string
DocURL
string
HideCcsImportButton
bool
HomeContent
string
HideCcsImportButton
bool
PurchaseSubscriptionEnabled
bool
PurchaseSubscriptionEnabled
bool
PurchaseSubscriptionURL
string
PurchaseSubscriptionURL
string
...
...
frontend/src/api/admin/settings.ts
View file @
7229b41f
...
@@ -14,6 +14,7 @@ export interface SystemSettings {
...
@@ -14,6 +14,7 @@ export interface SystemSettings {
email_verify_enabled
:
boolean
email_verify_enabled
:
boolean
promo_code_enabled
:
boolean
promo_code_enabled
:
boolean
password_reset_enabled
:
boolean
password_reset_enabled
:
boolean
invitation_code_enabled
:
boolean
totp_enabled
:
boolean
// TOTP 双因素认证
totp_enabled
:
boolean
// TOTP 双因素认证
totp_encryption_key_configured
:
boolean
// TOTP 加密密钥是否已配置
totp_encryption_key_configured
:
boolean
// TOTP 加密密钥是否已配置
// Default settings
// Default settings
...
@@ -72,6 +73,7 @@ export interface UpdateSettingsRequest {
...
@@ -72,6 +73,7 @@ export interface UpdateSettingsRequest {
email_verify_enabled
?:
boolean
email_verify_enabled
?:
boolean
promo_code_enabled
?:
boolean
promo_code_enabled
?:
boolean
password_reset_enabled
?:
boolean
password_reset_enabled
?:
boolean
invitation_code_enabled
?:
boolean
totp_enabled
?:
boolean
// TOTP 双因素认证
totp_enabled
?:
boolean
// TOTP 双因素认证
default_balance
?:
number
default_balance
?:
number
default_concurrency
?:
number
default_concurrency
?:
number
...
...
frontend/src/api/auth.ts
View file @
7229b41f
...
@@ -164,6 +164,24 @@ export async function validatePromoCode(code: string): Promise<ValidatePromoCode
...
@@ -164,6 +164,24 @@ export async function validatePromoCode(code: string): Promise<ValidatePromoCode
return
data
return
data
}
}
/**
* Validate invitation code response
*/
export
interface
ValidateInvitationCodeResponse
{
valid
:
boolean
error_code
?:
string
}
/**
* Validate invitation code (public endpoint, no auth required)
* @param code - Invitation code to validate
* @returns Validation result
*/
export
async
function
validateInvitationCode
(
code
:
string
):
Promise
<
ValidateInvitationCodeResponse
>
{
const
{
data
}
=
await
apiClient
.
post
<
ValidateInvitationCodeResponse
>
(
'
/auth/validate-invitation-code
'
,
{
code
})
return
data
}
/**
/**
* Forgot password request
* Forgot password request
*/
*/
...
@@ -229,6 +247,7 @@ export const authAPI = {
...
@@ -229,6 +247,7 @@ export const authAPI = {
getPublicSettings
,
getPublicSettings
,
sendVerifyCode
,
sendVerifyCode
,
validatePromoCode
,
validatePromoCode
,
validateInvitationCode
,
forgotPassword
,
forgotPassword
,
resetPassword
resetPassword
}
}
...
...
frontend/src/i18n/locales/en.ts
View file @
7229b41f
...
@@ -265,6 +265,13 @@ export default {
...
@@ -265,6 +265,13 @@ export default {
promoCodeAlreadyUsed
:
'
You have already used this promo code
'
,
promoCodeAlreadyUsed
:
'
You have already used this promo code
'
,
promoCodeValidating
:
'
Promo code is being validated, please wait
'
,
promoCodeValidating
:
'
Promo code is being validated, please wait
'
,
promoCodeInvalidCannotRegister
:
'
Invalid promo code. Please check and try again or clear the promo code field
'
,
promoCodeInvalidCannotRegister
:
'
Invalid promo code. Please check and try again or clear the promo code field
'
,
invitationCodeLabel
:
'
Invitation Code
'
,
invitationCodePlaceholder
:
'
Enter invitation code
'
,
invitationCodeRequired
:
'
Invitation code is required
'
,
invitationCodeValid
:
'
Invitation code is valid
'
,
invitationCodeInvalid
:
'
Invalid or used invitation code
'
,
invitationCodeValidating
:
'
Validating invitation code...
'
,
invitationCodeInvalidCannotRegister
:
'
Invalid invitation code. Please check and try again
'
,
linuxdo
:
{
linuxdo
:
{
signIn
:
'
Continue with Linux.do
'
,
signIn
:
'
Continue with Linux.do
'
,
orContinue
:
'
or continue with email
'
,
orContinue
:
'
or continue with email
'
,
...
@@ -1899,6 +1906,8 @@ export default {
...
@@ -1899,6 +1906,8 @@ export default {
balance
:
'
Balance
'
,
balance
:
'
Balance
'
,
concurrency
:
'
Concurrency
'
,
concurrency
:
'
Concurrency
'
,
subscription
:
'
Subscription
'
,
subscription
:
'
Subscription
'
,
invitation
:
'
Invitation
'
,
invitationHint
:
'
Invitation codes are used to restrict user registration. They are automatically marked as used after use.
'
,
unused
:
'
Unused
'
,
unused
:
'
Unused
'
,
used
:
'
Used
'
,
used
:
'
Used
'
,
columns
:
{
columns
:
{
...
@@ -1945,6 +1954,7 @@ export default {
...
@@ -1945,6 +1954,7 @@ export default {
balance
:
'
Balance
'
,
balance
:
'
Balance
'
,
concurrency
:
'
Concurrency
'
,
concurrency
:
'
Concurrency
'
,
subscription
:
'
Subscription
'
,
subscription
:
'
Subscription
'
,
invitation
:
'
Invitation
'
,
// Admin adjustment types (created when admin modifies user balance/concurrency)
// Admin adjustment types (created when admin modifies user balance/concurrency)
admin_balance
:
'
Balance (Admin)
'
,
admin_balance
:
'
Balance (Admin)
'
,
admin_concurrency
:
'
Concurrency (Admin)
'
admin_concurrency
:
'
Concurrency (Admin)
'
...
@@ -2896,6 +2906,8 @@ export default {
...
@@ -2896,6 +2906,8 @@ export default {
emailVerificationHint
:
'
Require email verification for new registrations
'
,
emailVerificationHint
:
'
Require email verification for new registrations
'
,
promoCode
:
'
Promo Code
'
,
promoCode
:
'
Promo Code
'
,
promoCodeHint
:
'
Allow users to use promo codes during registration
'
,
promoCodeHint
:
'
Allow users to use promo codes during registration
'
,
invitationCode
:
'
Invitation Code Registration
'
,
invitationCodeHint
:
'
When enabled, users must enter a valid invitation code to register
'
,
passwordReset
:
'
Password Reset
'
,
passwordReset
:
'
Password Reset
'
,
passwordResetHint
:
'
Allow users to reset their password via email
'
,
passwordResetHint
:
'
Allow users to reset their password via email
'
,
totp
:
'
Two-Factor Authentication (2FA)
'
,
totp
:
'
Two-Factor Authentication (2FA)
'
,
...
...
frontend/src/i18n/locales/zh.ts
View file @
7229b41f
...
@@ -262,6 +262,13 @@ export default {
...
@@ -262,6 +262,13 @@ export default {
promoCodeAlreadyUsed
:
'
您已使用过此优惠码
'
,
promoCodeAlreadyUsed
:
'
您已使用过此优惠码
'
,
promoCodeValidating
:
'
优惠码正在验证中,请稍候
'
,
promoCodeValidating
:
'
优惠码正在验证中,请稍候
'
,
promoCodeInvalidCannotRegister
:
'
优惠码无效,请检查后重试或清空优惠码
'
,
promoCodeInvalidCannotRegister
:
'
优惠码无效,请检查后重试或清空优惠码
'
,
invitationCodeLabel
:
'
邀请码
'
,
invitationCodePlaceholder
:
'
请输入邀请码
'
,
invitationCodeRequired
:
'
请输入邀请码
'
,
invitationCodeValid
:
'
邀请码有效
'
,
invitationCodeInvalid
:
'
邀请码无效或已被使用
'
,
invitationCodeValidating
:
'
正在验证邀请码...
'
,
invitationCodeInvalidCannotRegister
:
'
邀请码无效,请检查后重试
'
,
linuxdo
:
{
linuxdo
:
{
signIn
:
'
使用 Linux.do 登录
'
,
signIn
:
'
使用 Linux.do 登录
'
,
orContinue
:
'
或使用邮箱密码继续
'
,
orContinue
:
'
或使用邮箱密码继续
'
,
...
@@ -2022,6 +2029,7 @@ export default {
...
@@ -2022,6 +2029,7 @@ export default {
balance
:
'
余额
'
,
balance
:
'
余额
'
,
concurrency
:
'
并发数
'
,
concurrency
:
'
并发数
'
,
subscription
:
'
订阅
'
,
subscription
:
'
订阅
'
,
invitation
:
'
邀请码
'
,
// 管理员在用户管理页面调整余额/并发时产生的记录
// 管理员在用户管理页面调整余额/并发时产生的记录
admin_balance
:
'
余额(管理员)
'
,
admin_balance
:
'
余额(管理员)
'
,
admin_concurrency
:
'
并发数(管理员)
'
admin_concurrency
:
'
并发数(管理员)
'
...
@@ -2030,6 +2038,8 @@ export default {
...
@@ -2030,6 +2038,8 @@ export default {
balance
:
'
余额
'
,
balance
:
'
余额
'
,
concurrency
:
'
并发数
'
,
concurrency
:
'
并发数
'
,
subscription
:
'
订阅
'
,
subscription
:
'
订阅
'
,
invitation
:
'
邀请码
'
,
invitationHint
:
'
邀请码用于限制用户注册,使用后自动标记为已使用。
'
,
allTypes
:
'
全部类型
'
,
allTypes
:
'
全部类型
'
,
allStatus
:
'
全部状态
'
,
allStatus
:
'
全部状态
'
,
unused
:
'
未使用
'
,
unused
:
'
未使用
'
,
...
@@ -3049,6 +3059,8 @@ export default {
...
@@ -3049,6 +3059,8 @@ export default {
emailVerificationHint
:
'
新用户注册时需要验证邮箱
'
,
emailVerificationHint
:
'
新用户注册时需要验证邮箱
'
,
promoCode
:
'
优惠码
'
,
promoCode
:
'
优惠码
'
,
promoCodeHint
:
'
允许用户在注册时使用优惠码
'
,
promoCodeHint
:
'
允许用户在注册时使用优惠码
'
,
invitationCode
:
'
邀请码注册
'
,
invitationCodeHint
:
'
开启后,用户注册时需要填写有效的邀请码
'
,
passwordReset
:
'
忘记密码
'
,
passwordReset
:
'
忘记密码
'
,
passwordResetHint
:
'
允许用户通过邮箱重置密码
'
,
passwordResetHint
:
'
允许用户通过邮箱重置密码
'
,
totp
:
'
双因素认证 (2FA)
'
,
totp
:
'
双因素认证 (2FA)
'
,
...
...
Prev
1
2
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment