Full Go backend + Vue 3 frontend, OpenAI-compatible API, multi-provider account pools, billing/admin, Docker one-command deploy with auto HTTPS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
162 lines
4.1 KiB
Go
162 lines
4.1 KiB
Go
package service
|
|
|
|
import (
|
|
"errors"
|
|
"net/mail"
|
|
"regexp"
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
const (
|
|
MinUsernameLength = 6
|
|
MaxUsernameLength = 24
|
|
MinPasswordLength = 8
|
|
MaxPasswordLength = 24
|
|
)
|
|
|
|
var (
|
|
usernamePattern = regexp.MustCompile(`^[A-Za-z0-9]{6,24}$`)
|
|
emailCodePattern = regexp.MustCompile(`^\d{6}$`)
|
|
)
|
|
|
|
func ValidateEmail(email string) (string, error) {
|
|
normalized := strings.TrimSpace(strings.ToLower(email))
|
|
if normalized == "" {
|
|
return "", errors.New("邮箱不能为空")
|
|
}
|
|
if len(normalized) > 254 {
|
|
return "", errors.New("邮箱长度不能超过 254 个字符")
|
|
}
|
|
addr, err := mail.ParseAddress(normalized)
|
|
if err != nil || strings.TrimSpace(strings.ToLower(addr.Address)) != normalized {
|
|
return "", errors.New("邮箱格式不正确")
|
|
}
|
|
local, domain, ok := strings.Cut(normalized, "@")
|
|
if !ok || local == "" || domain == "" || strings.Contains(domain, "..") || !strings.Contains(domain, ".") {
|
|
return "", errors.New("邮箱格式不正确")
|
|
}
|
|
return normalized, nil
|
|
}
|
|
|
|
func ValidateUsername(username string) (string, error) {
|
|
normalized := strings.TrimSpace(username)
|
|
if normalized == "" {
|
|
return "", errors.New("用户名不能为空")
|
|
}
|
|
length := utf8.RuneCountInString(normalized)
|
|
if length < MinUsernameLength || length > MaxUsernameLength {
|
|
return "", errors.New("用户名长度需为 6 到 24 个字符")
|
|
}
|
|
if !usernamePattern.MatchString(normalized) {
|
|
return "", errors.New("用户名只能使用字母和数字")
|
|
}
|
|
return normalized, nil
|
|
}
|
|
|
|
func ValidatePassword(password string) error {
|
|
length := utf8.RuneCountInString(password)
|
|
if length < MinPasswordLength || length > MaxPasswordLength {
|
|
return errors.New("密码长度需为 8 到 24 个字符")
|
|
}
|
|
|
|
var hasLetter bool
|
|
var hasUpper bool
|
|
var hasLower bool
|
|
var hasDigit bool
|
|
var hasSymbol bool
|
|
for _, r := range password {
|
|
if unicode.IsSpace(r) {
|
|
return errors.New("密码不能包含空白字符")
|
|
}
|
|
if !isAllowedPasswordRune(r) {
|
|
return errors.New("密码包含不允许的字符")
|
|
}
|
|
if unicode.IsLetter(r) {
|
|
hasLetter = true
|
|
if unicode.IsUpper(r) {
|
|
hasUpper = true
|
|
}
|
|
if unicode.IsLower(r) {
|
|
hasLower = true
|
|
}
|
|
}
|
|
if unicode.IsDigit(r) {
|
|
hasDigit = true
|
|
}
|
|
if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
|
|
hasSymbol = true
|
|
}
|
|
}
|
|
if !hasLetter || !hasUpper || !hasLower || !hasDigit || !hasSymbol {
|
|
return errors.New("密码必须同时包含大写字母、小写字母、数字和符号")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isAllowedPasswordRune(r rune) bool {
|
|
if unicode.IsLetter(r) || unicode.IsDigit(r) {
|
|
return true
|
|
}
|
|
switch r {
|
|
case '(', ')', '~', '!', '@', '#', '$', '%', '^', '&', '*', '-', '_', '+', '=', '|',
|
|
'{', '}', '[', ']', ':', ';', '\'', '<', '>', ',', '.', '?', '/':
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func ValidateEmailCode(code string) (string, error) {
|
|
normalized := strings.TrimSpace(code)
|
|
if !emailCodePattern.MatchString(normalized) {
|
|
return "", errors.New("邮箱验证码必须是 6 位纯数字")
|
|
}
|
|
return normalized, nil
|
|
}
|
|
|
|
func ValidateLoginIdentifier(identifier string) (string, error) {
|
|
normalized := strings.TrimSpace(identifier)
|
|
if normalized == "" {
|
|
return "", errors.New("账号不能为空")
|
|
}
|
|
if strings.Contains(normalized, "@") {
|
|
return ValidateEmail(normalized)
|
|
}
|
|
return ValidateUsername(normalized)
|
|
}
|
|
|
|
func ValidateAllowedEmailDomains(domains []string) []string {
|
|
out := make([]string, 0, len(domains))
|
|
seen := map[string]struct{}{}
|
|
for _, raw := range domains {
|
|
normalized := strings.TrimSpace(strings.ToLower(strings.TrimPrefix(raw, "@")))
|
|
if normalized == "" || strings.Contains(normalized, " ") {
|
|
continue
|
|
}
|
|
if _, ok := seen[normalized]; ok {
|
|
continue
|
|
}
|
|
seen[normalized] = struct{}{}
|
|
out = append(out, normalized)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func EmailDomainAllowed(email string, domains []string) bool {
|
|
if len(domains) == 0 {
|
|
return true
|
|
}
|
|
_, domain, ok := strings.Cut(strings.ToLower(strings.TrimSpace(email)), "@")
|
|
if !ok {
|
|
return false
|
|
}
|
|
for _, allowed := range ValidateAllowedEmailDomains(domains) {
|
|
if domain == allowed {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|