Initial open-source release (MIT): image2api AI gateway
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>
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user