74 lines
2.5 KiB
Go
74 lines
2.5 KiB
Go
package booking
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// Same caps as internal/client/internal/order — problem_description ends up
|
|
// in an order's own field once confirmed (internal/pdfgen renders it via
|
|
// MultiCell), so the resource-exhaustion rationale there applies here too.
|
|
const (
|
|
maxShortFieldLen = 255
|
|
maxLongFieldLen = 5000
|
|
// preferredAtGrace tolerates client-side clock skew and the time
|
|
// between form-fill and submit — a request for "right now" shouldn't
|
|
// bounce just because a few minutes passed.
|
|
preferredAtGrace = 15 * time.Minute
|
|
)
|
|
|
|
// cleanField strips carriage returns/newlines and trims whitespace — same
|
|
// rationale as site's internal/lead.cleanField: unsanitized text ends up
|
|
// verbatim in the staff Telegram alert (notify.Send), and a raw newline
|
|
// could spoof extra "fields" in that message.
|
|
func cleanField(s string) string {
|
|
replacer := strings.NewReplacer("\r", " ", "\n", " ")
|
|
return strings.TrimSpace(replacer.Replace(s))
|
|
}
|
|
|
|
type createInput struct {
|
|
Name string `json:"name"`
|
|
Phone string `json:"phone"`
|
|
DeviceType string `json:"device_type"`
|
|
ProblemDescription string `json:"problem_description"`
|
|
PreferredAt string `json:"preferred_at"`
|
|
// Address/IsOnsite are only ever set via CreateByStaff (the "+Заявка"
|
|
// type-selector's "Выездной ремонт" branch) — the public Create handler
|
|
// never reads or sets them, so they default to zero values there.
|
|
Address string `json:"address"`
|
|
IsOnsite bool `json:"is_onsite"`
|
|
// Website is a honeypot field, same convention as site's lead form —
|
|
// real visitors never see or fill it (hidden off-screen). A non-empty
|
|
// value means a bot filled every field it could find.
|
|
Website string `json:"website"`
|
|
}
|
|
|
|
func (b createInput) validate() string {
|
|
if b.Name == "" {
|
|
return "name is required"
|
|
}
|
|
if b.Phone == "" {
|
|
return "phone is required"
|
|
}
|
|
if b.DeviceType == "" {
|
|
return "device_type is required"
|
|
}
|
|
for _, f := range []string{b.Name, b.Phone, b.DeviceType} {
|
|
if utf8.RuneCountInString(f) > maxShortFieldLen {
|
|
return "one of the fields is too long"
|
|
}
|
|
}
|
|
if utf8.RuneCountInString(b.ProblemDescription) > maxLongFieldLen {
|
|
return "problem_description is too long"
|
|
}
|
|
t, err := time.Parse(time.RFC3339, b.PreferredAt)
|
|
if err != nil {
|
|
return "preferred_at must be a valid date/time"
|
|
}
|
|
if t.Before(time.Now().Add(-preferredAtGrace)) {
|
|
return "preferred_at must not be in the past"
|
|
}
|
|
return ""
|
|
}
|