All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 5m23s
Co-Authored-By: claude-flow <ruv@ruv.net>
145 lines
3.9 KiB
Go
145 lines
3.9 KiB
Go
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
|
||
}
|