hi-server/internal/logic/auth/trialEmailWhitelist.go
shanshanzhong 23a7a292ef
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 5m23s
fix: Gmail 泛域名邮箱(含点号/+别名)直接拒绝赠送试用
Co-Authored-By: claude-flow <ruv@ruv.net>
2026-04-21 01:09:34 -07:00

145 lines
3.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.
// Returns false if the email format is invalid.
func IsEmailDomainWhitelisted(email, whitelistCSV string) bool {
if whitelistCSV == "" {
return false
}
parts := strings.SplitN(email, "@", 2)
if len(parts) != 2 {
return false
}
domain := strings.ToLower(strings.TrimSpace(parts[1]))
for _, d := range strings.Split(whitelistCSV, ",") {
if strings.ToLower(strings.TrimSpace(d)) == domain {
return true
}
}
return false
}
func ShouldGrantTrialForEmail(register config.RegisterConfig, email string) bool {
if !register.EnableTrial {
return false
}
if !register.EnableTrialEmailWhitelist {
return true
}
if register.TrialEmailDomainWhitelist == "" {
return false
}
if !IsEmailDomainWhitelisted(email, register.TrialEmailDomainWhitelist) {
return false
}
// Gmail 系域名local part 含点号或 + 别名的视为泛域名,直接拒绝赠送
if IsDisposableAlias(email) {
return false
}
return true
}
// IsDisposableAlias detects Gmail dot trick and + alias abuse.
// For Gmail-like domains, local part containing "." or "+" is rejected.
// For all other domains, only "+" alias is rejected.
func IsDisposableAlias(email string) bool {
parts := strings.SplitN(strings.ToLower(strings.TrimSpace(email)), "@", 2)
if len(parts) != 2 {
return false
}
local, domain := parts[0], parts[1]
// All domains: reject + alias
if strings.ContainsRune(local, '+') {
return true
}
// Gmail-like domains: reject dots in local part
if isGmailLikeDomain(domain) && strings.ContainsRune(local, '.') {
return true
}
return false
}
// 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
}