package auth import ( "strings" "galo/internal/httpx" ) // Identifier kinds for login and registration. const ( IdentifierEmail = "email" IdentifierPhone = "phone" ) // NormalizePhone validates and canonicalises a phone number to E.164: a leading // plus followed by 8–15 digits. It is the single normalisation point used by // registration, identifier addition and login, so lookups compare canonical // values only. func NormalizePhone(raw string) (string, error) { s := strings.TrimSpace(raw) if s == "" { return "", httpx.BadRequest("invalid_phone", "phone is required") } if !strings.HasPrefix(s, "+") { return "", httpx.BadRequest("invalid_phone", "phone must be in E.164 format with a leading +") } digits := s[1:] if len(digits) < 8 || len(digits) > 15 { return "", httpx.BadRequest("invalid_phone", "phone must contain between 8 and 15 digits") } for _, r := range digits { if r < '0' || r > '9' { return "", httpx.BadRequest("invalid_phone", "phone may contain digits only after the leading +") } } return s, nil } func validEmail(s string) bool { i := strings.Index(s, "@") return i > 0 && i < len(s)-1 && !strings.ContainsAny(s, " \t\r\n") } // NormalizeIdentifier classifies a login identifier as email (contains `@`) or // phone, returning the canonical value for lookups. func NormalizeIdentifier(raw string) (kind, value string, err error) { s := strings.TrimSpace(raw) if s == "" { return "", "", httpx.BadRequest("invalid_identifier", "identifier is required") } if strings.Contains(s, "@") { v := strings.ToLower(s) if !validEmail(v) { return "", "", httpx.BadRequest("invalid_identifier", "valid email is required") } return IdentifierEmail, v, nil } v, err := NormalizePhone(s) if err != nil { return "", "", err } return IdentifierPhone, v, nil }