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
6c86501d
Commit
6c86501d
authored
Jan 29, 2026
by
shuike
Browse files
feat: 增加邀请码注册功能
parent
0ab68aa9
Changes
26
Hide whitespace changes
Inline
Side-by-side
backend/cmd/jwtgen/main.go
View file @
6c86501d
...
...
@@ -33,7 +33,7 @@ func main() {
}()
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
)
defer
cancel
()
...
...
backend/cmd/server/wire_gen.go
View file @
6c86501d
...
...
@@ -43,6 +43,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
return
nil
,
err
}
userRepository
:=
repository
.
NewUserRepository
(
client
,
db
)
redeemCodeRepository
:=
repository
.
NewRedeemCodeRepository
(
client
)
settingRepository
:=
repository
.
NewSettingRepository
(
client
)
settingService
:=
service
.
NewSettingService
(
settingRepository
,
configConfig
)
redisClient
:=
repository
.
ProvideRedis
(
configConfig
)
...
...
@@ -61,24 +62,23 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
apiKeyService
:=
service
.
NewAPIKeyService
(
apiKeyRepository
,
userRepository
,
groupRepository
,
userSubscriptionRepository
,
apiKeyCache
,
configConfig
)
apiKeyAuthCacheInvalidator
:=
service
.
ProvideAPIKeyAuthCacheInvalidator
(
apiKeyService
)
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
)
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
)
if
err
!=
nil
{
return
nil
,
err
}
totpCache
:=
repository
.
NewTotpCache
(
redisClient
)
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
)
apiKeyHandler
:=
handler
.
NewAPIKeyHandler
(
apiKeyService
)
usageLogRepository
:=
repository
.
NewUsageLogRepository
(
client
,
db
)
usageService
:=
service
.
NewUsageService
(
usageLogRepository
,
userRepository
,
client
,
apiKeyAuthCacheInvalidator
)
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
)
subscriptionHandler
:=
handler
.
NewSubscriptionHandler
(
subscriptionService
)
announcementRepository
:=
repository
.
NewAnnouncementRepository
(
client
)
...
...
backend/internal/domain/constants.go
View file @
6c86501d
...
...
@@ -36,6 +36,7 @@ const (
RedeemTypeBalance
=
"balance"
RedeemTypeConcurrency
=
"concurrency"
RedeemTypeSubscription
=
"subscription"
RedeemTypeInvitation
=
"invitation"
)
// PromoCode status constants
...
...
backend/internal/handler/admin/redeem_handler.go
View file @
6c86501d
...
...
@@ -29,7 +29,7 @@ func NewRedeemHandler(adminService service.AdminService) *RedeemHandler {
// GenerateRedeemCodesRequest represents generate redeem codes request
type
GenerateRedeemCodesRequest
struct
{
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"`
GroupID
*
int64
`json:"group_id"`
// 订阅类型必填
ValidityDays
int
`json:"validity_days" binding:"omitempty,max=36500"`
// 订阅类型使用,默认30天,最大100年
...
...
backend/internal/handler/admin/setting_handler.go
View file @
6c86501d
...
...
@@ -49,6 +49,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
EmailVerifyEnabled
:
settings
.
EmailVerifyEnabled
,
PromoCodeEnabled
:
settings
.
PromoCodeEnabled
,
PasswordResetEnabled
:
settings
.
PasswordResetEnabled
,
InvitationCodeEnabled
:
settings
.
InvitationCodeEnabled
,
TotpEnabled
:
settings
.
TotpEnabled
,
TotpEncryptionKeyConfigured
:
h
.
settingService
.
IsTotpEncryptionKeyConfigured
(),
SMTPHost
:
settings
.
SMTPHost
,
...
...
@@ -94,11 +95,12 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
// UpdateSettingsRequest 更新设置请求
type
UpdateSettingsRequest
struct
{
// 注册设置
RegistrationEnabled
bool
`json:"registration_enabled"`
EmailVerifyEnabled
bool
`json:"email_verify_enabled"`
PromoCodeEnabled
bool
`json:"promo_code_enabled"`
PasswordResetEnabled
bool
`json:"password_reset_enabled"`
TotpEnabled
bool
`json:"totp_enabled"`
// TOTP 双因素认证
RegistrationEnabled
bool
`json:"registration_enabled"`
EmailVerifyEnabled
bool
`json:"email_verify_enabled"`
PromoCodeEnabled
bool
`json:"promo_code_enabled"`
PasswordResetEnabled
bool
`json:"password_reset_enabled"`
InvitationCodeEnabled
bool
`json:"invitation_code_enabled"`
TotpEnabled
bool
`json:"totp_enabled"`
// TOTP 双因素认证
// 邮件服务设置
SMTPHost
string
`json:"smtp_host"`
...
...
@@ -291,6 +293,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
EmailVerifyEnabled
:
req
.
EmailVerifyEnabled
,
PromoCodeEnabled
:
req
.
PromoCodeEnabled
,
PasswordResetEnabled
:
req
.
PasswordResetEnabled
,
InvitationCodeEnabled
:
req
.
InvitationCodeEnabled
,
TotpEnabled
:
req
.
TotpEnabled
,
SMTPHost
:
req
.
SMTPHost
,
SMTPPort
:
req
.
SMTPPort
,
...
...
@@ -370,6 +373,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
EmailVerifyEnabled
:
updatedSettings
.
EmailVerifyEnabled
,
PromoCodeEnabled
:
updatedSettings
.
PromoCodeEnabled
,
PasswordResetEnabled
:
updatedSettings
.
PasswordResetEnabled
,
InvitationCodeEnabled
:
updatedSettings
.
InvitationCodeEnabled
,
TotpEnabled
:
updatedSettings
.
TotpEnabled
,
TotpEncryptionKeyConfigured
:
h
.
settingService
.
IsTotpEncryptionKeyConfigured
(),
SMTPHost
:
updatedSettings
.
SMTPHost
,
...
...
backend/internal/handler/auth_handler.go
View file @
6c86501d
...
...
@@ -15,23 +15,25 @@ import (
// AuthHandler handles authentication-related requests
type
AuthHandler
struct
{
cfg
*
config
.
Config
authService
*
service
.
AuthService
userService
*
service
.
UserService
settingSvc
*
service
.
SettingService
promoService
*
service
.
PromoService
totpService
*
service
.
TotpService
cfg
*
config
.
Config
authService
*
service
.
AuthService
userService
*
service
.
UserService
settingSvc
*
service
.
SettingService
promoService
*
service
.
PromoService
redeemService
*
service
.
RedeemService
totpService
*
service
.
TotpService
}
// 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
{
cfg
:
cfg
,
authService
:
authService
,
userService
:
userService
,
settingSvc
:
settingService
,
promoService
:
promoService
,
totpService
:
totpService
,
cfg
:
cfg
,
authService
:
authService
,
userService
:
userService
,
settingSvc
:
settingService
,
promoService
:
promoService
,
redeemService
:
redeemService
,
totpService
:
totpService
,
}
}
...
...
@@ -41,7 +43,8 @@ type RegisterRequest struct {
Password
string
`json:"password" binding:"required,min=6"`
VerifyCode
string
`json:"verify_code"`
TurnstileToken
string
`json:"turnstile_token"`
PromoCode
string
`json:"promo_code"`
// 注册优惠码
PromoCode
string
`json:"promo_code"`
// 注册优惠码
InvitationCode
string
`json:"invitation_code"`
// 邀请码
}
// SendVerifyCodeRequest 发送验证码请求
...
...
@@ -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
{
response
.
ErrorFrom
(
c
,
err
)
return
...
...
@@ -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 忘记密码请求
type
ForgotPasswordRequest
struct
{
Email
string
`json:"email" binding:"required,email"`
...
...
backend/internal/handler/dto/settings.go
View file @
6c86501d
...
...
@@ -6,6 +6,7 @@ type SystemSettings struct {
EmailVerifyEnabled
bool
`json:"email_verify_enabled"`
PromoCodeEnabled
bool
`json:"promo_code_enabled"`
PasswordResetEnabled
bool
`json:"password_reset_enabled"`
InvitationCodeEnabled
bool
`json:"invitation_code_enabled"`
TotpEnabled
bool
`json:"totp_enabled"`
// TOTP 双因素认证
TotpEncryptionKeyConfigured
bool
`json:"totp_encryption_key_configured"`
// TOTP 加密密钥是否已配置
...
...
@@ -63,6 +64,7 @@ type PublicSettings struct {
EmailVerifyEnabled
bool
`json:"email_verify_enabled"`
PromoCodeEnabled
bool
`json:"promo_code_enabled"`
PasswordResetEnabled
bool
`json:"password_reset_enabled"`
InvitationCodeEnabled
bool
`json:"invitation_code_enabled"`
TotpEnabled
bool
`json:"totp_enabled"`
// TOTP 双因素认证
TurnstileEnabled
bool
`json:"turnstile_enabled"`
TurnstileSiteKey
string
`json:"turnstile_site_key"`
...
...
backend/internal/handler/setting_handler.go
View file @
6c86501d
...
...
@@ -36,6 +36,7 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) {
EmailVerifyEnabled
:
settings
.
EmailVerifyEnabled
,
PromoCodeEnabled
:
settings
.
PromoCodeEnabled
,
PasswordResetEnabled
:
settings
.
PasswordResetEnabled
,
InvitationCodeEnabled
:
settings
.
InvitationCodeEnabled
,
TotpEnabled
:
settings
.
TotpEnabled
,
TurnstileEnabled
:
settings
.
TurnstileEnabled
,
TurnstileSiteKey
:
settings
.
TurnstileSiteKey
,
...
...
backend/internal/server/api_contract_test.go
View file @
6c86501d
...
...
@@ -600,7 +600,7 @@ func newContractDeps(t *testing.T) *contractDeps {
settingService
:=
service
.
NewSettingService
(
settingRepo
,
cfg
)
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
)
usageHandler
:=
handler
.
NewUsageHandler
(
usageService
,
apiKeyService
)
adminSettingHandler
:=
adminhandler
.
NewSettingHandler
(
settingService
,
nil
,
nil
,
nil
)
...
...
backend/internal/server/routes/auth.go
View file @
6c86501d
...
...
@@ -32,6 +32,10 @@ func RegisterAuthRoutes(
auth
.
POST
(
"/validate-promo-code"
,
rateLimiter
.
LimitWithOptions
(
"validate-promo"
,
10
,
time
.
Minute
,
middleware
.
RateLimitOptions
{
FailureMode
:
middleware
.
RateLimitFailClose
,
}),
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)
auth
.
POST
(
"/forgot-password"
,
rateLimiter
.
LimitWithOptions
(
"forgot-password"
,
5
,
time
.
Minute
,
middleware
.
RateLimitOptions
{
FailureMode
:
middleware
.
RateLimitFailClose
,
...
...
backend/internal/service/auth_service.go
View file @
6c86501d
...
...
@@ -19,17 +19,19 @@ import (
)
var
(
ErrInvalidCredentials
=
infraerrors
.
Unauthorized
(
"INVALID_CREDENTIALS"
,
"invalid email or password"
)
ErrUserNotActive
=
infraerrors
.
Forbidden
(
"USER_NOT_ACTIVE"
,
"user is not active"
)
ErrEmailExists
=
infraerrors
.
Conflict
(
"EMAIL_EXISTS"
,
"email already exists"
)
ErrEmailReserved
=
infraerrors
.
BadRequest
(
"EMAIL_RESERVED"
,
"email is reserved"
)
ErrInvalidToken
=
infraerrors
.
Unauthorized
(
"INVALID_TOKEN"
,
"invalid token"
)
ErrTokenExpired
=
infraerrors
.
Unauthorized
(
"TOKEN_EXPIRED"
,
"token has expired"
)
ErrTokenTooLarge
=
infraerrors
.
BadRequest
(
"TOKEN_TOO_LARGE"
,
"token too large"
)
ErrTokenRevoked
=
infraerrors
.
Unauthorized
(
"TOKEN_REVOKED"
,
"token has been revoked"
)
ErrEmailVerifyRequired
=
infraerrors
.
BadRequest
(
"EMAIL_VERIFY_REQUIRED"
,
"email verification is required"
)
ErrRegDisabled
=
infraerrors
.
Forbidden
(
"REGISTRATION_DISABLED"
,
"registration is currently disabled"
)
ErrServiceUnavailable
=
infraerrors
.
ServiceUnavailable
(
"SERVICE_UNAVAILABLE"
,
"service temporarily unavailable"
)
ErrInvalidCredentials
=
infraerrors
.
Unauthorized
(
"INVALID_CREDENTIALS"
,
"invalid email or password"
)
ErrUserNotActive
=
infraerrors
.
Forbidden
(
"USER_NOT_ACTIVE"
,
"user is not active"
)
ErrEmailExists
=
infraerrors
.
Conflict
(
"EMAIL_EXISTS"
,
"email already exists"
)
ErrEmailReserved
=
infraerrors
.
BadRequest
(
"EMAIL_RESERVED"
,
"email is reserved"
)
ErrInvalidToken
=
infraerrors
.
Unauthorized
(
"INVALID_TOKEN"
,
"invalid token"
)
ErrTokenExpired
=
infraerrors
.
Unauthorized
(
"TOKEN_EXPIRED"
,
"token has expired"
)
ErrTokenTooLarge
=
infraerrors
.
BadRequest
(
"TOKEN_TOO_LARGE"
,
"token too large"
)
ErrTokenRevoked
=
infraerrors
.
Unauthorized
(
"TOKEN_REVOKED"
,
"token has been revoked"
)
ErrEmailVerifyRequired
=
infraerrors
.
BadRequest
(
"EMAIL_VERIFY_REQUIRED"
,
"email verification is required"
)
ErrRegDisabled
=
infraerrors
.
Forbidden
(
"REGISTRATION_DISABLED"
,
"registration is currently disabled"
)
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 触发解析时的异常内存分配。
...
...
@@ -47,6 +49,7 @@ type JWTClaims struct {
// AuthService 认证服务
type
AuthService
struct
{
userRepo
UserRepository
redeemRepo
RedeemCodeRepository
cfg
*
config
.
Config
settingService
*
SettingService
emailService
*
EmailService
...
...
@@ -58,6 +61,7 @@ type AuthService struct {
// NewAuthService 创建认证服务实例
func
NewAuthService
(
userRepo
UserRepository
,
redeemRepo
RedeemCodeRepository
,
cfg
*
config
.
Config
,
settingService
*
SettingService
,
emailService
*
EmailService
,
...
...
@@ -67,6 +71,7 @@ func NewAuthService(
)
*
AuthService
{
return
&
AuthService
{
userRepo
:
userRepo
,
redeemRepo
:
redeemRepo
,
cfg
:
cfg
,
settingService
:
settingService
,
emailService
:
emailService
,
...
...
@@ -78,11 +83,11 @@ func NewAuthService(
// Register 用户注册,返回token和用户
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和用户
func
(
s
*
AuthService
)
RegisterWithVerification
(
ctx
context
.
Context
,
email
,
password
,
verifyCode
,
promoCode
string
)
(
string
,
*
User
,
error
)
{
// RegisterWithVerification 用户注册(支持邮件验证
、
优惠码
和邀请码
),返回token和用户
func
(
s
*
AuthService
)
RegisterWithVerification
(
ctx
context
.
Context
,
email
,
password
,
verifyCode
,
promoCode
,
invitationCode
string
)
(
string
,
*
User
,
error
)
{
// 检查是否开放注册(默认关闭:settingService 未配置时不允许注册)
if
s
.
settingService
==
nil
||
!
s
.
settingService
.
IsRegistrationEnabled
(
ctx
)
{
return
""
,
nil
,
ErrRegDisabled
...
...
@@ -93,6 +98,26 @@ func (s *AuthService) RegisterWithVerification(ctx context.Context, email, passw
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
)
{
// 如果邮件验证已开启但邮件服务未配置,拒绝注册
...
...
@@ -153,6 +178,14 @@ func (s *AuthService) RegisterWithVerification(ctx context.Context, email, passw
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
err
:=
s
.
promoService
.
ApplyPromoCode
(
ctx
,
user
.
ID
,
promoCode
);
err
!=
nil
{
...
...
backend/internal/service/auth_service_register_test.go
View file @
6c86501d
...
...
@@ -115,6 +115,7 @@ func newAuthService(repo *userRepoStub, settings map[string]string, emailCache E
return
NewAuthService
(
repo
,
nil
,
// redeemRepo
cfg
,
settingService
,
emailService
,
...
...
@@ -152,7 +153,7 @@ func TestAuthService_Register_EmailVerifyEnabledButServiceNotConfigured(t *testi
},
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
)
}
...
...
@@ -164,7 +165,7 @@ func TestAuthService_Register_EmailVerifyRequired(t *testing.T) {
SettingKeyEmailVerifyEnabled
:
"true"
,
},
cache
)
_
,
_
,
err
:=
service
.
RegisterWithVerification
(
context
.
Background
(),
"user@test.com"
,
"password"
,
""
,
""
)
_
,
_
,
err
:=
service
.
RegisterWithVerification
(
context
.
Background
(),
"user@test.com"
,
"password"
,
""
,
""
,
""
)
require
.
ErrorIs
(
t
,
err
,
ErrEmailVerifyRequired
)
}
...
...
@@ -178,7 +179,7 @@ func TestAuthService_Register_EmailVerifyInvalid(t *testing.T) {
SettingKeyEmailVerifyEnabled
:
"true"
,
},
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
.
ErrorContains
(
t
,
err
,
"verify code"
)
}
...
...
backend/internal/service/domain_constants.go
View file @
6c86501d
...
...
@@ -38,6 +38,7 @@ const (
RedeemTypeBalance
=
domain
.
RedeemTypeBalance
RedeemTypeConcurrency
=
domain
.
RedeemTypeConcurrency
RedeemTypeSubscription
=
domain
.
RedeemTypeSubscription
RedeemTypeInvitation
=
domain
.
RedeemTypeInvitation
)
// PromoCode status constants
...
...
@@ -71,10 +72,11 @@ const LinuxDoConnectSyntheticEmailDomain = "@linuxdo-connect.invalid"
// Setting keys
const
(
// 注册设置
SettingKeyRegistrationEnabled
=
"registration_enabled"
// 是否开放注册
SettingKeyEmailVerifyEnabled
=
"email_verify_enabled"
// 是否开启邮件验证
SettingKeyPromoCodeEnabled
=
"promo_code_enabled"
// 是否启用优惠码功能
SettingKeyPasswordResetEnabled
=
"password_reset_enabled"
// 是否启用忘记密码功能(需要先开启邮件验证)
SettingKeyRegistrationEnabled
=
"registration_enabled"
// 是否开放注册
SettingKeyEmailVerifyEnabled
=
"email_verify_enabled"
// 是否开启邮件验证
SettingKeyPromoCodeEnabled
=
"promo_code_enabled"
// 是否启用优惠码功能
SettingKeyPasswordResetEnabled
=
"password_reset_enabled"
// 是否启用忘记密码功能(需要先开启邮件验证)
SettingKeyInvitationCodeEnabled
=
"invitation_code_enabled"
// 是否启用邀请码注册
// 邮件服务设置
SettingKeySMTPHost
=
"smtp_host"
// SMTP服务器地址
...
...
backend/internal/service/redeem_service.go
View file @
6c86501d
...
...
@@ -126,7 +126,8 @@ func (s *RedeemService) GenerateCodes(ctx context.Context, req GenerateCodesRequ
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"
)
}
...
...
@@ -139,6 +140,12 @@ func (s *RedeemService) GenerateCodes(ctx context.Context, req GenerateCodesRequ
codeType
=
RedeemTypeBalance
}
// 邀请码类型的 value 设为 0
value
:=
req
.
Value
if
codeType
==
RedeemTypeInvitation
{
value
=
0
}
codes
:=
make
([]
RedeemCode
,
0
,
req
.
Count
)
for
i
:=
0
;
i
<
req
.
Count
;
i
++
{
code
,
err
:=
s
.
GenerateRandomCode
()
...
...
@@ -149,7 +156,7 @@ func (s *RedeemService) GenerateCodes(ctx context.Context, req GenerateCodesRequ
codes
=
append
(
codes
,
RedeemCode
{
Code
:
code
,
Type
:
codeType
,
Value
:
req
.
V
alue
,
Value
:
v
alue
,
Status
:
StatusUnused
,
})
}
...
...
backend/internal/service/setting_service.go
View file @
6c86501d
...
...
@@ -62,6 +62,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
SettingKeyEmailVerifyEnabled
,
SettingKeyPromoCodeEnabled
,
SettingKeyPasswordResetEnabled
,
SettingKeyInvitationCodeEnabled
,
SettingKeyTotpEnabled
,
SettingKeyTurnstileEnabled
,
SettingKeyTurnstileSiteKey
,
...
...
@@ -99,6 +100,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
EmailVerifyEnabled
:
emailVerifyEnabled
,
PromoCodeEnabled
:
settings
[
SettingKeyPromoCodeEnabled
]
!=
"false"
,
// 默认启用
PasswordResetEnabled
:
passwordResetEnabled
,
InvitationCodeEnabled
:
settings
[
SettingKeyInvitationCodeEnabled
]
==
"true"
,
TotpEnabled
:
settings
[
SettingKeyTotpEnabled
]
==
"true"
,
TurnstileEnabled
:
settings
[
SettingKeyTurnstileEnabled
]
==
"true"
,
TurnstileSiteKey
:
settings
[
SettingKeyTurnstileSiteKey
],
...
...
@@ -141,6 +143,7 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
EmailVerifyEnabled
bool
`json:"email_verify_enabled"`
PromoCodeEnabled
bool
`json:"promo_code_enabled"`
PasswordResetEnabled
bool
`json:"password_reset_enabled"`
InvitationCodeEnabled
bool
`json:"invitation_code_enabled"`
TotpEnabled
bool
`json:"totp_enabled"`
TurnstileEnabled
bool
`json:"turnstile_enabled"`
TurnstileSiteKey
string
`json:"turnstile_site_key,omitempty"`
...
...
@@ -161,6 +164,7 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
EmailVerifyEnabled
:
settings
.
EmailVerifyEnabled
,
PromoCodeEnabled
:
settings
.
PromoCodeEnabled
,
PasswordResetEnabled
:
settings
.
PasswordResetEnabled
,
InvitationCodeEnabled
:
settings
.
InvitationCodeEnabled
,
TotpEnabled
:
settings
.
TotpEnabled
,
TurnstileEnabled
:
settings
.
TurnstileEnabled
,
TurnstileSiteKey
:
settings
.
TurnstileSiteKey
,
...
...
@@ -188,6 +192,7 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet
updates
[
SettingKeyEmailVerifyEnabled
]
=
strconv
.
FormatBool
(
settings
.
EmailVerifyEnabled
)
updates
[
SettingKeyPromoCodeEnabled
]
=
strconv
.
FormatBool
(
settings
.
PromoCodeEnabled
)
updates
[
SettingKeyPasswordResetEnabled
]
=
strconv
.
FormatBool
(
settings
.
PasswordResetEnabled
)
updates
[
SettingKeyInvitationCodeEnabled
]
=
strconv
.
FormatBool
(
settings
.
InvitationCodeEnabled
)
updates
[
SettingKeyTotpEnabled
]
=
strconv
.
FormatBool
(
settings
.
TotpEnabled
)
// 邮件服务设置(只有非空才更新密码)
...
...
@@ -286,6 +291,15 @@ func (s *SettingService) IsPromoCodeEnabled(ctx context.Context) bool {
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 检查是否启用密码重置功能
// 要求:必须同时开启邮件验证
func
(
s
*
SettingService
)
IsPasswordResetEnabled
(
ctx
context
.
Context
)
bool
{
...
...
@@ -401,6 +415,7 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
EmailVerifyEnabled
:
emailVerifyEnabled
,
PromoCodeEnabled
:
settings
[
SettingKeyPromoCodeEnabled
]
!=
"false"
,
// 默认启用
PasswordResetEnabled
:
emailVerifyEnabled
&&
settings
[
SettingKeyPasswordResetEnabled
]
==
"true"
,
InvitationCodeEnabled
:
settings
[
SettingKeyInvitationCodeEnabled
]
==
"true"
,
TotpEnabled
:
settings
[
SettingKeyTotpEnabled
]
==
"true"
,
SMTPHost
:
settings
[
SettingKeySMTPHost
],
SMTPUsername
:
settings
[
SettingKeySMTPUsername
],
...
...
backend/internal/service/settings_view.go
View file @
6c86501d
package
service
type
SystemSettings
struct
{
RegistrationEnabled
bool
EmailVerifyEnabled
bool
PromoCodeEnabled
bool
PasswordResetEnabled
bool
TotpEnabled
bool
// TOTP 双因素认证
RegistrationEnabled
bool
EmailVerifyEnabled
bool
PromoCodeEnabled
bool
PasswordResetEnabled
bool
InvitationCodeEnabled
bool
TotpEnabled
bool
// TOTP 双因素认证
SMTPHost
string
SMTPPort
int
...
...
@@ -61,21 +62,22 @@ type SystemSettings struct {
}
type
PublicSettings
struct
{
RegistrationEnabled
bool
EmailVerifyEnabled
bool
PromoCodeEnabled
bool
PasswordResetEnabled
bool
TotpEnabled
bool
// TOTP 双因素认证
TurnstileEnabled
bool
TurnstileSiteKey
string
SiteName
string
SiteLogo
string
SiteSubtitle
string
APIBaseURL
string
ContactInfo
string
DocURL
string
HomeContent
string
HideCcsImportButton
bool
RegistrationEnabled
bool
EmailVerifyEnabled
bool
PromoCodeEnabled
bool
PasswordResetEnabled
bool
InvitationCodeEnabled
bool
TotpEnabled
bool
// TOTP 双因素认证
TurnstileEnabled
bool
TurnstileSiteKey
string
SiteName
string
SiteLogo
string
SiteSubtitle
string
APIBaseURL
string
ContactInfo
string
DocURL
string
HomeContent
string
HideCcsImportButton
bool
PurchaseSubscriptionEnabled
bool
PurchaseSubscriptionURL
string
...
...
frontend/src/api/admin/settings.ts
View file @
6c86501d
...
...
@@ -14,6 +14,7 @@ export interface SystemSettings {
email_verify_enabled
:
boolean
promo_code_enabled
:
boolean
password_reset_enabled
:
boolean
invitation_code_enabled
:
boolean
totp_enabled
:
boolean
// TOTP 双因素认证
totp_encryption_key_configured
:
boolean
// TOTP 加密密钥是否已配置
// Default settings
...
...
@@ -72,6 +73,7 @@ export interface UpdateSettingsRequest {
email_verify_enabled
?:
boolean
promo_code_enabled
?:
boolean
password_reset_enabled
?:
boolean
invitation_code_enabled
?:
boolean
totp_enabled
?:
boolean
// TOTP 双因素认证
default_balance
?:
number
default_concurrency
?:
number
...
...
frontend/src/api/auth.ts
View file @
6c86501d
...
...
@@ -164,6 +164,24 @@ export async function validatePromoCode(code: string): Promise<ValidatePromoCode
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
*/
...
...
@@ -229,6 +247,7 @@ export const authAPI = {
getPublicSettings
,
sendVerifyCode
,
validatePromoCode
,
validateInvitationCode
,
forgotPassword
,
resetPassword
}
...
...
frontend/src/i18n/locales/en.ts
View file @
6c86501d
...
...
@@ -265,6 +265,13 @@ export default {
promoCodeAlreadyUsed
:
'
You have already used this promo code
'
,
promoCodeValidating
:
'
Promo code is being validated, please wait
'
,
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
:
{
signIn
:
'
Continue with Linux.do
'
,
orContinue
:
'
or continue with email
'
,
...
...
@@ -1899,6 +1906,8 @@ export default {
balance
:
'
Balance
'
,
concurrency
:
'
Concurrency
'
,
subscription
:
'
Subscription
'
,
invitation
:
'
Invitation
'
,
invitationHint
:
'
Invitation codes are used to restrict user registration. They are automatically marked as used after use.
'
,
unused
:
'
Unused
'
,
used
:
'
Used
'
,
columns
:
{
...
...
@@ -1945,6 +1954,7 @@ export default {
balance
:
'
Balance
'
,
concurrency
:
'
Concurrency
'
,
subscription
:
'
Subscription
'
,
invitation
:
'
Invitation
'
,
// Admin adjustment types (created when admin modifies user balance/concurrency)
admin_balance
:
'
Balance (Admin)
'
,
admin_concurrency
:
'
Concurrency (Admin)
'
...
...
@@ -2896,6 +2906,8 @@ export default {
emailVerificationHint
:
'
Require email verification for new registrations
'
,
promoCode
:
'
Promo Code
'
,
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
'
,
passwordResetHint
:
'
Allow users to reset their password via email
'
,
totp
:
'
Two-Factor Authentication (2FA)
'
,
...
...
frontend/src/i18n/locales/zh.ts
View file @
6c86501d
...
...
@@ -262,6 +262,13 @@ export default {
promoCodeAlreadyUsed
:
'
您已使用过此优惠码
'
,
promoCodeValidating
:
'
优惠码正在验证中,请稍候
'
,
promoCodeInvalidCannotRegister
:
'
优惠码无效,请检查后重试或清空优惠码
'
,
invitationCodeLabel
:
'
邀请码
'
,
invitationCodePlaceholder
:
'
请输入邀请码
'
,
invitationCodeRequired
:
'
请输入邀请码
'
,
invitationCodeValid
:
'
邀请码有效
'
,
invitationCodeInvalid
:
'
邀请码无效或已被使用
'
,
invitationCodeValidating
:
'
正在验证邀请码...
'
,
invitationCodeInvalidCannotRegister
:
'
邀请码无效,请检查后重试
'
,
linuxdo
:
{
signIn
:
'
使用 Linux.do 登录
'
,
orContinue
:
'
或使用邮箱密码继续
'
,
...
...
@@ -2022,6 +2029,7 @@ export default {
balance
:
'
余额
'
,
concurrency
:
'
并发数
'
,
subscription
:
'
订阅
'
,
invitation
:
'
邀请码
'
,
// 管理员在用户管理页面调整余额/并发时产生的记录
admin_balance
:
'
余额(管理员)
'
,
admin_concurrency
:
'
并发数(管理员)
'
...
...
@@ -2030,6 +2038,8 @@ export default {
balance
:
'
余额
'
,
concurrency
:
'
并发数
'
,
subscription
:
'
订阅
'
,
invitation
:
'
邀请码
'
,
invitationHint
:
'
邀请码用于限制用户注册,使用后自动标记为已使用。
'
,
allTypes
:
'
全部类型
'
,
allStatus
:
'
全部状态
'
,
unused
:
'
未使用
'
,
...
...
@@ -3049,6 +3059,8 @@ export default {
emailVerificationHint
:
'
新用户注册时需要验证邮箱
'
,
promoCode
:
'
优惠码
'
,
promoCodeHint
:
'
允许用户在注册时使用优惠码
'
,
invitationCode
:
'
邀请码注册
'
,
invitationCodeHint
:
'
开启后,用户注册时需要填写有效的邀请码
'
,
passwordReset
:
'
忘记密码
'
,
passwordResetHint
:
'
允许用户通过邮箱重置密码
'
,
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