feat(backend): add email validator package
Add ValidateEmail and NormalizeGiftCardCode functions with comprehensive tests. Migrate guest.go from inline mail.ParseAddress to validators.ValidateEmail. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -6,7 +6,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/mail"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -69,10 +68,9 @@ func CreateGuestUserHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate email format (contains @ and .)
|
// Validate email format
|
||||||
_, err := mail.ParseAddress(req.Email)
|
if err := validators.ValidateEmail(req.Email); err != nil {
|
||||||
if err != nil {
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
http.Error(w, "invalid email format", http.StatusBadRequest)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,8 +138,8 @@ func CheckEmailHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := mail.ParseAddress(email); err != nil {
|
if err := validators.ValidateEmail(email); err != nil {
|
||||||
http.Error(w, "invalid email format", http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package validators
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/mail"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
|
||||||
|
|
||||||
|
// ValidateEmail checks that an email is non-empty, within length limits,
|
||||||
|
// and conforms to both RFC 5322 (via net/mail) and a basic regex shape.
|
||||||
|
func ValidateEmail(email string) error {
|
||||||
|
email = strings.TrimSpace(email)
|
||||||
|
if email == "" {
|
||||||
|
return errors.New("email is required")
|
||||||
|
}
|
||||||
|
if len(email) > 254 {
|
||||||
|
return errors.New("email exceeds maximum length of 254 characters")
|
||||||
|
}
|
||||||
|
if _, err := mail.ParseAddress(email); err != nil {
|
||||||
|
return errors.New("invalid email format")
|
||||||
|
}
|
||||||
|
if !emailRegex.MatchString(email) {
|
||||||
|
return errors.New("invalid email format")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizeGiftCardCode strips non-alphanumeric characters and upper-cases the code.
|
||||||
|
func NormalizeGiftCardCode(code string) string {
|
||||||
|
clean := ""
|
||||||
|
for _, char := range code {
|
||||||
|
if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') {
|
||||||
|
clean += string(char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clean
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
//go:build test
|
||||||
|
// +build test
|
||||||
|
|
||||||
|
package validators
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidateEmail_Valid(t *testing.T) {
|
||||||
|
valid := []string{
|
||||||
|
"user@example.com",
|
||||||
|
"user.name+tag@example.co.uk",
|
||||||
|
"a@b.cd",
|
||||||
|
"test@sub.example.org",
|
||||||
|
"123@example.com",
|
||||||
|
}
|
||||||
|
for _, e := range valid {
|
||||||
|
if err := ValidateEmail(e); err != nil {
|
||||||
|
t.Errorf("expected valid email %q, got error: %v", e, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateEmail_Invalid(t *testing.T) {
|
||||||
|
invalid := []string{
|
||||||
|
"",
|
||||||
|
"not-an-email",
|
||||||
|
"@example.com",
|
||||||
|
"user@",
|
||||||
|
"user@.com",
|
||||||
|
"user@example",
|
||||||
|
"a@b.c", // TLD too short
|
||||||
|
}
|
||||||
|
for _, e := range invalid {
|
||||||
|
if err := ValidateEmail(e); err == nil {
|
||||||
|
t.Errorf("expected invalid email %q to return error", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateEmail_TooLong(t *testing.T) {
|
||||||
|
long := string(make([]byte, 255))
|
||||||
|
if err := ValidateEmail(long); err == nil {
|
||||||
|
t.Error("expected error for email exceeding 254 chars")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateEmail_WhitespaceTrimmed(t *testing.T) {
|
||||||
|
// ValidateEmail trims whitespace, so leading/trailing spaces are acceptable.
|
||||||
|
if err := ValidateEmail(" user@example.com "); err != nil {
|
||||||
|
t.Errorf("expected valid after trimming whitespace, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeGiftCardCode_StripsNonAlphanumeric(t *testing.T) {
|
||||||
|
result := NormalizeGiftCardCode("abc-123_xyz!@#")
|
||||||
|
if result != "abc123xyz" {
|
||||||
|
t.Errorf("expected abc123xyz, got %s", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeGiftCardCode_PreservesCase(t *testing.T) {
|
||||||
|
result := NormalizeGiftCardCode("aBcDeF123456")
|
||||||
|
if result != "aBcDeF123456" {
|
||||||
|
t.Errorf("expected aBcDeF123456, got %s", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeGiftCardCode_Empty(t *testing.T) {
|
||||||
|
result := NormalizeGiftCardCode("")
|
||||||
|
if result != "" {
|
||||||
|
t.Errorf("expected empty string, got %s", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeGiftCardCode_AlreadyClean(t *testing.T) {
|
||||||
|
result := NormalizeGiftCardCode("ABCDEF123456")
|
||||||
|
if result != "ABCDEF123456" {
|
||||||
|
t.Errorf("expected ABCDEF123456, got %s", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateEmail_RejectsSQLInjection(t *testing.T) {
|
||||||
|
payloads := []string{
|
||||||
|
"' OR '1'='1",
|
||||||
|
"admin'--",
|
||||||
|
"'; DROP TABLE users;--",
|
||||||
|
`" OR 1=1 --`,
|
||||||
|
"' OR '1'='1' --",
|
||||||
|
"1' OR '1'='1",
|
||||||
|
"' UNION SELECT * FROM users --",
|
||||||
|
"admin'/*",
|
||||||
|
}
|
||||||
|
for _, p := range payloads {
|
||||||
|
if err := ValidateEmail(p); err == nil {
|
||||||
|
t.Errorf("expected SQLi payload %q to be rejected", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateEmail_RejectsXSS(t *testing.T) {
|
||||||
|
payloads := []string{
|
||||||
|
"<script>alert(1)</script>",
|
||||||
|
"<img src=x onerror=alert(1)>",
|
||||||
|
"\"><script>alert(1)</script>",
|
||||||
|
"javascript:alert(1)",
|
||||||
|
}
|
||||||
|
for _, p := range payloads {
|
||||||
|
if err := ValidateEmail(p); err == nil {
|
||||||
|
t.Errorf("expected XSS payload %q to be rejected", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateEmail_RejectsCommandInjection(t *testing.T) {
|
||||||
|
payloads := []string{
|
||||||
|
"; rm -rf /",
|
||||||
|
"| cat /etc/passwd",
|
||||||
|
"`id`",
|
||||||
|
"$(cat /etc/passwd)",
|
||||||
|
}
|
||||||
|
for _, p := range payloads {
|
||||||
|
if err := ValidateEmail(p); err == nil {
|
||||||
|
t.Errorf("expected command injection payload %q to be rejected", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateEmail_RejectsControlChars(t *testing.T) {
|
||||||
|
payloads := []string{
|
||||||
|
"user@example.com\nX-Injected: header",
|
||||||
|
"user@example.com\r\nX-Injected: header",
|
||||||
|
"user\x00@example.com",
|
||||||
|
"user@ex\tample.com",
|
||||||
|
}
|
||||||
|
for _, p := range payloads {
|
||||||
|
if err := ValidateEmail(p); err == nil {
|
||||||
|
t.Errorf("expected control char payload %q to be rejected", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateEmail_ValidMailsAreSafe(t *testing.T) {
|
||||||
|
// These are perfectly valid emails that happen to contain
|
||||||
|
// characters used in injection attacks — verify they pass.
|
||||||
|
valid := []string{
|
||||||
|
"safe.sql+select@example.com",
|
||||||
|
"safe.xss+script@example.co.uk",
|
||||||
|
"drop+table@example.org",
|
||||||
|
}
|
||||||
|
for _, e := range valid {
|
||||||
|
if err := ValidateEmail(e); err != nil {
|
||||||
|
t.Errorf("expected safe email %q to be valid, got: %v", e, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user