feat: 邮箱规范化(NormalizeEmail)与域名白名单检查(IsEmailDomainWhitelisted)
Build docker and publish / build (20.15.1) (push) Has been cancelled
Build docker and publish / build (20.15.1) (push) Has been cancelled
This commit is contained in:
@@ -126,7 +126,7 @@ func (l *EmailLoginLogic) EmailLogin(req *types.EmailLoginRequest) (resp *types.
|
||||
return err
|
||||
}
|
||||
rc := l.svcCtx.Config.Register
|
||||
if ShouldGrantTrialForEmail(rc, req.Email) {
|
||||
if ShouldGrantTrialForEmail(rc, req.Email) && !NormalizedEmailHasTrial(l.ctx, l.svcCtx.DB, req.Email, rc.TrialSubscribe) {
|
||||
if err = l.activeTrial(userInfo.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -396,7 +396,7 @@ func (l *OAuthLoginGetTokenLogic) register(email, avatar, method, openid, reques
|
||||
}
|
||||
|
||||
rc := l.svcCtx.Config.Register
|
||||
shouldActivateTrial := email != "" && authlogic.ShouldGrantTrialForEmail(rc, email)
|
||||
shouldActivateTrial := email != "" && authlogic.ShouldGrantTrialForEmail(rc, email) && !authlogic.NormalizedEmailHasTrial(l.ctx, l.svcCtx.DB, email, rc.TrialSubscribe)
|
||||
|
||||
if shouldActivateTrial {
|
||||
l.Debugw("activating trial subscription",
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/perfect-panel/server/internal/config"
|
||||
usermodel "github.com/perfect-panel/server/internal/model/user"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// IsEmailDomainWhitelisted checks if the email's domain is in the comma-separated whitelist.
|
||||
@@ -37,3 +40,77 @@ func ShouldGrantTrialForEmail(register config.RegisterConfig, email string) bool
|
||||
}
|
||||
return IsEmailDomainWhitelisted(email, register.TrialEmailDomainWhitelist)
|
||||
}
|
||||
|
||||
// NormalizeEmail returns a canonical form of the email for trial deduplication.
|
||||
// Strips "+" aliases universally (user+tag@any.com → user@any.com).
|
||||
// Removes dots from local part for Gmail-like providers (gmail.com, googlemail.com).
|
||||
func NormalizeEmail(email string) string {
|
||||
email = strings.ToLower(strings.TrimSpace(email))
|
||||
parts := strings.SplitN(email, "@", 2)
|
||||
if len(parts) != 2 {
|
||||
return email
|
||||
}
|
||||
local, domain := parts[0], parts[1]
|
||||
|
||||
// Strip + alias
|
||||
if idx := strings.IndexByte(local, '+'); idx != -1 {
|
||||
local = local[:idx]
|
||||
}
|
||||
|
||||
// Remove dots for Gmail-like providers that ignore dots in local part
|
||||
if isGmailLikeDomain(domain) {
|
||||
local = strings.ReplaceAll(local, ".", "")
|
||||
}
|
||||
|
||||
return local + "@" + domain
|
||||
}
|
||||
|
||||
func isGmailLikeDomain(domain string) bool {
|
||||
switch domain {
|
||||
case "gmail.com", "googlemail.com":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NormalizedEmailHasTrial returns true if any user with the same normalized email
|
||||
// already holds a trial subscription. Only performs the cross-user DB check when
|
||||
// normalization actually changes the email (i.e., dots removed or + alias stripped).
|
||||
func NormalizedEmailHasTrial(ctx context.Context, db *gorm.DB, email string, trialSubscribeId int64) bool {
|
||||
normalized := NormalizeEmail(email)
|
||||
if normalized == strings.ToLower(strings.TrimSpace(email)) {
|
||||
return false // normalization changed nothing, skip cross-user check
|
||||
}
|
||||
|
||||
parts := strings.SplitN(normalized, "@", 2)
|
||||
if len(parts) != 2 {
|
||||
return false
|
||||
}
|
||||
domain := parts[1]
|
||||
|
||||
var authMethods []usermodel.AuthMethods
|
||||
if err := db.WithContext(ctx).
|
||||
Model(&usermodel.AuthMethods{}).
|
||||
Select("user_id, auth_identifier").
|
||||
Where("auth_type = ? AND auth_identifier LIKE ?", "email", "%@"+domain).
|
||||
Find(&authMethods).Error; err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, am := range authMethods {
|
||||
if NormalizeEmail(am.AuthIdentifier) != normalized {
|
||||
continue
|
||||
}
|
||||
var count int64
|
||||
if err := db.WithContext(ctx).
|
||||
Model(&usermodel.Subscribe{}).
|
||||
Where("user_id = ? AND subscribe_id = ?", am.UserId, trialSubscribeId).
|
||||
Count(&count).Error; err != nil {
|
||||
continue
|
||||
}
|
||||
if count > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
|
||||
|
||||
// Activate trial subscription after transaction success (moved outside transaction to reduce lock time)
|
||||
rc := l.svcCtx.Config.Register
|
||||
if ShouldGrantTrialForEmail(rc, req.Email) {
|
||||
if ShouldGrantTrialForEmail(rc, req.Email) && !NormalizedEmailHasTrial(l.ctx, l.svcCtx.DB, req.Email, rc.TrialSubscribe) {
|
||||
trialSubscribe, err = l.activeTrial(userInfo.Id)
|
||||
if err != nil {
|
||||
l.Errorw("Failed to activate trial subscription", logger.Field("error", err.Error()))
|
||||
|
||||
@@ -39,6 +39,16 @@ func tryGrantTrialOnEmailBind(ctx context.Context, svcCtx *svc.ServiceContext, l
|
||||
return
|
||||
}
|
||||
|
||||
// Cross-user check: prevent the same real inbox (via dot trick / + alias) from
|
||||
// getting multiple trials across different accounts.
|
||||
if auth.NormalizedEmailHasTrial(ctx, svcCtx.DB, email, rc.TrialSubscribe) {
|
||||
log.Infow("normalized email already has trial via another account, skip",
|
||||
logger.Field("email", email),
|
||||
logger.Field("owner_user_id", ownerUserId),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
sub, err := svcCtx.SubscribeModel.FindOne(ctx, rc.TrialSubscribe)
|
||||
if err != nil {
|
||||
log.Errorw("failed to find trial subscribe template", logger.Field("error", err.Error()))
|
||||
|
||||
Reference in New Issue
Block a user