All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 5m29s
202 lines
5.4 KiB
Go
202 lines
5.4 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"net/mail"
|
|
"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
|
|
}
|
|
_, domain, ok := parseStrictEmail(email)
|
|
if !ok {
|
|
return false
|
|
}
|
|
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 !IsValidTrialEmail(email) {
|
|
return false
|
|
}
|
|
// 无论白名单是否启用,泛域名邮箱(含 + 别名或 Gmail 点号)始终拒绝赠送
|
|
if IsDisposableAlias(email) {
|
|
return false
|
|
}
|
|
if isConfusableGmailDomain(emailDomain(email)) {
|
|
return false
|
|
}
|
|
if !register.EnableTrialEmailWhitelist {
|
|
return true
|
|
}
|
|
if register.TrialEmailDomainWhitelist == "" {
|
|
return false
|
|
}
|
|
if !IsEmailDomainWhitelisted(email, register.TrialEmailDomainWhitelist) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// ShouldAutoGrantTrialOnPublicEmailFlows defines whether browser/email-originated
|
|
// flows may auto-create a trial subscription. The current policy disables trial
|
|
// creation for email registration, email login auto-register, OAuth-with-email,
|
|
// and email binding/verification to avoid abuse through public email channels.
|
|
func ShouldAutoGrantTrialOnPublicEmailFlows(register config.RegisterConfig) bool {
|
|
return false
|
|
}
|
|
|
|
// 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 {
|
|
local, domain, ok := parseStrictEmail(email)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
// 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))
|
|
local, domain, ok := parseStrictEmail(email)
|
|
if !ok {
|
|
return email
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
func IsValidTrialEmail(email string) bool {
|
|
local, domain, ok := parseStrictEmail(email)
|
|
if !ok {
|
|
return false
|
|
}
|
|
return local != "" && domain != ""
|
|
}
|
|
|
|
func parseStrictEmail(email string) (local, domain string, ok bool) {
|
|
email = strings.ToLower(strings.TrimSpace(email))
|
|
if email == "" || strings.ContainsAny(email, " \t\r\n") {
|
|
return "", "", false
|
|
}
|
|
addr, err := mail.ParseAddress(email)
|
|
if err != nil || addr.Address != email || addr.Name != "" {
|
|
return "", "", false
|
|
}
|
|
parts := strings.Split(addr.Address, "@")
|
|
if len(parts) != 2 {
|
|
return "", "", false
|
|
}
|
|
local = strings.TrimSpace(parts[0])
|
|
domain = strings.Trim(strings.TrimSpace(parts[1]), ".")
|
|
if local == "" || domain == "" || strings.Contains(domain, "..") || !strings.Contains(domain, ".") {
|
|
return "", "", false
|
|
}
|
|
return local, domain, true
|
|
}
|
|
|
|
func emailDomain(email string) string {
|
|
_, domain, ok := parseStrictEmail(email)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return domain
|
|
}
|
|
|
|
func isConfusableGmailDomain(domain string) bool {
|
|
switch strings.ToLower(strings.TrimSpace(domain)) {
|
|
case "gmaial.com", "gmial.com", "gmai.com", "gamil.com", "gmal.com", "gmail.co", "gmail.con":
|
|
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
|
|
}
|