package handlers import ( "context" "fmt" "net/http" "strings" "time" "git.haelnorr.com/h/golib/hws" "git.haelnorr.com/h/oslstats/internal/db" "git.haelnorr.com/h/oslstats/internal/view/page" "github.com/pkg/errors" "github.com/uptrace/bun" ) func NewSeason( s *hws.Server, conn *bun.DB, ) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { renderSafely(page.NewSeason(), s, r, w) return } }) } func NewSeasonSubmit( s *hws.Server, conn *bun.DB, ) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Parse form data err := r.ParseForm() if err != nil { err = notifyWarn(s, r, "Invalid Form", "Please check your input and try again.", nil) if err != nil { throwInternalServiceError(s, w, r, "Error notifying client", err) } return } // Get form values name := strings.TrimSpace(r.FormValue("name")) shortName := strings.TrimSpace(strings.ToUpper(r.FormValue("short_name"))) startDateStr := r.FormValue("start_date") // Validate required fields if name == "" || shortName == "" || startDateStr == "" { err = notifyWarn(s, r, "Missing Fields", "All fields are required.", nil) if err != nil { throwInternalServiceError(s, w, r, "Error notifying client", err) } return } // Validate field lengths if len(name) > 20 { err = notifyWarn(s, r, "Invalid Name", "Season name must be 20 characters or less.", nil) if err != nil { throwInternalServiceError(s, w, r, "Error notifying client", err) } return } if len(shortName) > 6 { err = notifyWarn(s, r, "Invalid Short Name", "Short name must be 6 characters or less.", nil) if err != nil { throwInternalServiceError(s, w, r, "Error notifying client", err) } return } // Validate short name is alphanumeric only if !isAlphanumeric(shortName) { err = notifyWarn(s, r, "Invalid Short Name", "Short name must contain only letters and numbers.", nil) if err != nil { throwInternalServiceError(s, w, r, "Error notifying client", err) } return } // Parse start date (DD/MM/YYYY format) startDate, err := time.Parse("02/01/2006", startDateStr) if err != nil { err = notifyWarn(s, r, "Invalid Date", "Please provide a valid start date in DD/MM/YYYY format.", nil) if err != nil { throwInternalServiceError(s, w, r, "Error notifying client", err) } return } // Begin database transaction ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) defer cancel() tx, err := conn.BeginTx(ctx, nil) if err != nil { err = notifyInternalServiceError(s, r, "Database error", errors.Wrap(err, "conn.BeginTx")) if err != nil { throwInternalServiceError(s, w, r, "Error notifying client", err) } return } defer tx.Rollback() // Double-check uniqueness (race condition protection) nameUnique, err := db.IsSeasonNameUnique(ctx, tx, name) if err != nil { err = notifyInternalServiceError(s, r, "Database error", errors.Wrap(err, "db.IsSeasonNameUnique")) if err != nil { throwInternalServiceError(s, w, r, "Error notifying client", err) } return } if !nameUnique { err = notifyWarn(s, r, "Duplicate Name", "This season name is already taken.", nil) if err != nil { throwInternalServiceError(s, w, r, "Error notifying client", err) } return } shortNameUnique, err := db.IsSeasonShortNameUnique(ctx, tx, shortName) if err != nil { err = notifyInternalServiceError(s, r, "Database error", errors.Wrap(err, "db.IsSeasonShortNameUnique")) if err != nil { throwInternalServiceError(s, w, r, "Error notifying client", err) } return } if !shortNameUnique { err = notifyWarn(s, r, "Duplicate Short Name", "This short name is already taken.", nil) if err != nil { throwInternalServiceError(s, w, r, "Error notifying client", err) } return } // Create the season season, err := db.NewSeason(ctx, tx, name, shortName, startDate) if err != nil { err = notifyInternalServiceError(s, r, "Failed to create season", errors.Wrap(err, "db.NewSeason")) if err != nil { throwInternalServiceError(s, w, r, "Error notifying client", err) } return } // Commit transaction err = tx.Commit() if err != nil { err = notifyInternalServiceError(s, r, "Database error", errors.Wrap(err, "tx.Commit")) if err != nil { throwInternalServiceError(s, w, r, "Error notifying client", err) } return } // Send success notification err = notifySuccess(s, r, "Season Created", fmt.Sprintf("Successfully created season: %s", name), nil) if err != nil { // Log but don't fail the request s.LogError(hws.HWSError{ StatusCode: http.StatusInternalServerError, Message: "Failed to send success notification", Error: err, }) } // Redirect to the season detail page w.Header().Set("HX-Redirect", fmt.Sprintf("/seasons/%s", season.ShortName)) w.WriteHeader(http.StatusOK) }) } // Helper function to validate alphanumeric strings func isAlphanumeric(s string) bool { for _, r := range s { if !((r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9')) { return false } } return true } func IsSeasonNameUnique( s *hws.Server, conn *bun.DB, ) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { w.WriteHeader(http.StatusBadRequest) return } ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) defer cancel() tx, err := conn.BeginTx(ctx, nil) if err != nil { err = notifyInternalServiceError(s, r, "Database error", errors.Wrap(err, "conn.BeginTx")) if err != nil { throwInternalServiceError(s, w, r, "Error notifying client", err) } return } defer tx.Rollback() // Trim whitespace for consistency name := strings.TrimSpace(r.FormValue("name")) unique, err := db.IsSeasonNameUnique(ctx, tx, name) if err != nil { err = notifyInternalServiceError(s, r, "Database error", errors.Wrap(err, "db.IsSeasonNameUnique")) if err != nil { throwInternalServiceError(s, w, r, "Error notifying client", err) } return } tx.Commit() if !unique { w.WriteHeader(http.StatusConflict) return } w.WriteHeader(http.StatusOK) }) } func IsSeasonShortNameUnique( s *hws.Server, conn *bun.DB, ) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { w.WriteHeader(http.StatusBadRequest) return } ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) defer cancel() tx, err := conn.BeginTx(ctx, nil) if err != nil { err = notifyInternalServiceError(s, r, "Database error", errors.Wrap(err, "conn.BeginTx")) if err != nil { throwInternalServiceError(s, w, r, "Error notifying client", err) } return } defer tx.Rollback() // Get short name and convert to uppercase for consistency shortname := strings.ToUpper(strings.TrimSpace(r.FormValue("short_name"))) unique, err := db.IsSeasonShortNameUnique(ctx, tx, shortname) if err != nil { err = notifyInternalServiceError(s, r, "Database error", errors.Wrap(err, "db.IsSeasonShortNameUnique")) if err != nil { throwInternalServiceError(s, w, r, "Error notifying client", err) } return } tx.Commit() if !unique { w.WriteHeader(http.StatusConflict) return } w.WriteHeader(http.StatusOK) }) }