feat: validate auth and user request structs

Ultraworked with Sisyphus (https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-05-31 10:54:48 +01:00
co-authored by Sisyphus
parent 8b72c3fbef
commit 605c1422b9
3 changed files with 65 additions and 32 deletions
+21 -10
View File
@@ -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))
+10 -4
View File
@@ -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)
+34 -18
View File
@@ -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"`
}