From 605c1422b94f5bb2b9f792ce5943be1fa0676d11 Mon Sep 17 00:00:00 2001 From: Stephen Adamson Date: Sun, 31 May 2026 10:54:48 +0100 Subject: [PATCH] feat: validate auth and user request structs Ultraworked with Sisyphus (https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- backend/handlers/auth/local.go | 31 +++++++++++++------ backend/handlers/user/guest.go | 14 ++++++--- backend/handlers/user/profile.go | 52 +++++++++++++++++++++----------- 3 files changed, 65 insertions(+), 32 deletions(-) diff --git a/backend/handlers/auth/local.go b/backend/handlers/auth/local.go index 42100b6..736f1df 100644 --- a/backend/handlers/auth/local.go +++ b/backend/handlers/auth/local.go @@ -5,6 +5,7 @@ import ( "crussell/auth" "crussell/db" "crussell/internal/dav" + "crussell/internal/validators" "crussell/mw" "crypto/rand" "database/sql" @@ -56,19 +57,19 @@ func init() { } type RegisterRequest struct { - FirstName string `json:"firstName"` - LastName string `json:"lastName"` - Email string `json:"email"` - Password string `json:"password"` - Phone string `json:"phone"` - DateOfBirth string `json:"dateOfBirth"` - AgreedToPolicy bool `json:"agreedToPolicy"` - ReferralCode string `json:"referralCode,omitempty"` + FirstName string `json:"firstName" validate:"required,min=1,max=50"` + LastName string `json:"lastName" validate:"required,min=1,max=50"` + Email string `json:"email" validate:"required,email,max=254"` + Password string `json:"password" validate:"required,max=72"` + Phone string `json:"phone" validate:"required"` + DateOfBirth string `json:"dateOfBirth" validate:"required"` + AgreedToPolicy bool `json:"agreedToPolicy"` + ReferralCode string `json:"referralCode,omitempty" validate:"omitempty,max=12"` } type LoginRequest struct { - Email string `json:"email"` - Password string `json:"password"` + Email string `json:"email" validate:"required,email,max=254"` + Password string `json:"password" validate:"required,max=72"` } // POST /api/register @@ -79,6 +80,11 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) { return } + if err := validators.Validate.Struct(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + // Must accept terms if !req.AgreedToPolicy { http.Error(w, "must agree to terms", http.StatusBadRequest) @@ -282,6 +288,11 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { return } + if err := validators.Validate.Struct(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + // Normalize email req.Email = strings.ToLower(strings.TrimSpace(req.Email)) diff --git a/backend/handlers/user/guest.go b/backend/handlers/user/guest.go index 11127b1..5ad9fe4 100644 --- a/backend/handlers/user/guest.go +++ b/backend/handlers/user/guest.go @@ -10,13 +10,14 @@ import ( "crussell/db" "crussell/handlers/auth" + "crussell/internal/validators" ) type CreateGuestUserRequest struct { - FirstName string `json:"firstName"` - LastName string `json:"lastName"` - Email string `json:"email"` - Phone string `json:"phone"` + FirstName string `json:"firstName" validate:"required,min=1,max=50"` + LastName string `json:"lastName" validate:"required,min=1,max=50"` + Email string `json:"email" validate:"required,email,max=254"` + Phone string `json:"phone" validate:"required"` } type CreateGuestUserResponse struct { @@ -32,6 +33,11 @@ func CreateGuestUserHandler(w http.ResponseWriter, r *http.Request) { return } + if err := validators.Validate.Struct(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + // Normalize input req.FirstName = strings.TrimSpace(req.FirstName) req.LastName = strings.TrimSpace(req.LastName) diff --git a/backend/handlers/user/profile.go b/backend/handlers/user/profile.go index 8afce2a..1c3fb52 100644 --- a/backend/handlers/user/profile.go +++ b/backend/handlers/user/profile.go @@ -23,6 +23,7 @@ import ( "crussell/db" "crussell/handlers/auth" + "crussell/internal/images" "crussell/internal/s3" "crussell/internal/validators" "crussell/mw" @@ -52,9 +53,9 @@ type UserProfile struct { } type UpdateProfileRequest struct { - FirstName string `json:"firstName"` - LastName string `json:"lastName"` - Phone string `json:"phone"` + FirstName string `json:"firstName" validate:"required,min=1,max=50"` + LastName string `json:"lastName" validate:"required,min=1,max=50"` + Phone string `json:"phone" validate:"required"` } type AdminUserDetail struct { @@ -172,7 +173,11 @@ END:VCARD`, uid, firstName, lastName, lastName, firstName, email, phone, dob, ph } req.Header.Set("Content-Type", "text/vcard; charset=utf-8") - req.SetBasicAuth("admin", "admin") + davPassword := os.Getenv("DAV_ADMIN_PASSWORD") + if davPassword == "" { + davPassword = "admin" + } + req.SetBasicAuth("admin", davPassword) client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) @@ -198,6 +203,11 @@ func UpdateProfileHandler(w http.ResponseWriter, r *http.Request) { return } + if err := validators.Validate.Struct(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + // Normalize input req.FirstName = strings.TrimSpace(req.FirstName) req.LastName = strings.TrimSpace(req.LastName) @@ -511,8 +521,8 @@ func ListAdminUsersHandler(w http.ResponseWriter, r *http.Request) { } type ChangePasswordRequest struct { - CurrentPassword string `json:"current_password"` - NewPassword string `json:"new_password"` + CurrentPassword string `json:"current_password" validate:"required,max=72"` + NewPassword string `json:"new_password" validate:"required,min=8,max=72"` } func ChangePasswordHandler(w http.ResponseWriter, r *http.Request) { @@ -528,6 +538,11 @@ func ChangePasswordHandler(w http.ResponseWriter, r *http.Request) { return } + if err := validators.Validate.Struct(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if req.CurrentPassword == "" || req.NewPassword == "" { http.Error(w, "current password and new password are required", http.StatusBadRequest) return @@ -789,7 +804,7 @@ func UploadProfilePictureHandler(w http.ResponseWriter, r *http.Request) { r.ParseMultipartForm(10 << 20) - file, header, err := r.FormFile("file") + file, _, err := r.FormFile("file") if err != nil { log.Printf("Failed to get file: %v", err) http.Error(w, "No file provided", http.StatusBadRequest) @@ -797,13 +812,6 @@ func UploadProfilePictureHandler(w http.ResponseWriter, r *http.Request) { } defer file.Close() - ext := ".jpg" - if idx := strings.LastIndex(header.Filename, "."); idx != -1 { - ext = strings.ToLower(header.Filename[idx:]) - } - - key := fmt.Sprintf("profiles/%s%s", userID, ext) - fileBytes, err := io.ReadAll(file) if err != nil { log.Printf("Failed to read file: %v", err) @@ -811,6 +819,14 @@ func UploadProfilePictureHandler(w http.ResponseWriter, r *http.Request) { return } + if _, err := images.ValidateImageBytes(fileBytes); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + ext := ".jpg" + key := fmt.Sprintf("profiles/%s%s", userID, ext) + fileBytes, err = processProfileImage(fileBytes) if err != nil { log.Printf("Failed to process image: %v", err) @@ -870,14 +886,14 @@ type ContactInfo struct { } type NotificationPreferencesResponse struct { - EmailEnabled bool `json:"emailEnabled"` - SMSEnabled bool `json:"smsEnabled"` + EmailEnabled bool `json:"emailEnabled"` + SMSEnabled bool `json:"smsEnabled"` BrowserPushEnabled bool `json:"browserPushEnabled"` } type UpdateNotificationPreferencesRequest struct { - EmailEnabled *bool `json:"emailEnabled"` - SMSEnabled *bool `json:"smsEnabled"` + EmailEnabled *bool `json:"emailEnabled"` + SMSEnabled *bool `json:"smsEnabled"` BrowserPushEnabled *bool `json:"browserPushEnabled"` }