Files
PenaltyTracker/pilots.go
T

216 lines
6.2 KiB
Go

package main
import (
"encoding/csv"
"encoding/json"
"io"
"net/http"
"strconv"
"strings"
)
func registerPilotRoutes(mux *http.ServeMux) {
mux.HandleFunc("GET /api/competitions/{id}/pilots", requireAuth(handleListPilots))
mux.HandleFunc("POST /api/competitions/{id}/pilots", requireAuth(handleCreatePilot))
mux.HandleFunc("PATCH /api/competitions/{id}/pilots/{pid}", requireAuth(handleUpdatePilot))
mux.HandleFunc("DELETE /api/competitions/{id}/pilots/{pid}", requireAuth(handleDeletePilot))
mux.HandleFunc("POST /api/competitions/{id}/pilots/import", requireAuth(handleImportPilots))
}
func requireChiefOrAdmin(r *http.Request, competitionID int64) bool {
u := userFromCtx(r)
role, ok := canAccessCompetition(u, competitionID)
if !ok {
return false
}
return role == "system_admin" || role == "chief_scorer"
}
func handleListPilots(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid_id")
return
}
u := userFromCtx(r)
if _, ok := canAccessCompetition(u, id); !ok {
writeError(w, http.StatusForbidden, "forbidden")
return
}
rows, err := db.Query("SELECT id,competition_id,number,last_name,first_name,country,balloon_id FROM pilots WHERE competition_id=? ORDER BY number", id)
if err != nil {
writeError(w, http.StatusInternalServerError, "db_error")
return
}
defer rows.Close()
out := []Pilot{}
for rows.Next() {
var p Pilot
rows.Scan(&p.ID, &p.CompetitionID, &p.Number, &p.LastName, &p.FirstName, &p.Country, &p.BalloonID)
out = append(out, p)
}
writeJSON(w, http.StatusOK, out)
}
func handleCreatePilot(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid_id")
return
}
if !requireChiefOrAdmin(r, id) {
writeError(w, http.StatusForbidden, "forbidden")
return
}
var p Pilot
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
writeError(w, http.StatusBadRequest, "invalid_body")
return
}
p.Number = strings.TrimSpace(p.Number)
if p.Number == "" || p.LastName == "" {
writeError(w, http.StatusBadRequest, "missing_fields")
return
}
if len(p.Number) > 32 || len(p.LastName) > 128 || len(p.FirstName) > 128 ||
len(p.Country) > 64 || len(p.BalloonID) > 64 {
writeError(w, http.StatusBadRequest, "too_long")
return
}
res, err := db.Exec(
"INSERT INTO pilots(competition_id,number,last_name,first_name,country,balloon_id) VALUES(?,?,?,?,?,?)",
id, p.Number, p.LastName, p.FirstName, p.Country, p.BalloonID,
)
if err != nil {
writeError(w, http.StatusConflict, "duplicate_number")
return
}
p.ID, _ = res.LastInsertId()
p.CompetitionID = id
hub.broadcast(id, "pilot_changed", nil)
writeJSON(w, http.StatusCreated, p)
}
func handleUpdatePilot(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid_id")
return
}
pid, err := strconv.ParseInt(r.PathValue("pid"), 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid_id")
return
}
if !requireChiefOrAdmin(r, id) {
writeError(w, http.StatusForbidden, "forbidden")
return
}
var p Pilot
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
writeError(w, http.StatusBadRequest, "invalid_body")
return
}
_, err = db.Exec(
"UPDATE pilots SET number=?,last_name=?,first_name=?,country=?,balloon_id=? WHERE id=? AND competition_id=?",
p.Number, p.LastName, p.FirstName, p.Country, p.BalloonID, pid, id,
)
if err != nil {
writeError(w, http.StatusInternalServerError, "db_error")
return
}
hub.broadcast(id, "pilot_changed", nil)
w.WriteHeader(http.StatusNoContent)
}
func handleDeletePilot(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid_id")
return
}
pid, err := strconv.ParseInt(r.PathValue("pid"), 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid_id")
return
}
if !requireChiefOrAdmin(r, id) {
writeError(w, http.StatusForbidden, "forbidden")
return
}
db.Exec("DELETE FROM pilots WHERE id=? AND competition_id=?", pid, id)
hub.broadcast(id, "pilot_changed", nil)
w.WriteHeader(http.StatusNoContent)
}
func handleImportPilots(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid_id")
return
}
if !requireChiefOrAdmin(r, id) {
writeError(w, http.StatusForbidden, "forbidden")
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
writeError(w, http.StatusBadRequest, "read_error")
return
}
reader := csv.NewReader(strings.NewReader(string(body)))
reader.FieldsPerRecord = -1
records, err := reader.ReadAll()
if err != nil {
writeError(w, http.StatusBadRequest, "invalid_csv")
return
}
tx, err := db.Begin()
if err != nil {
writeError(w, http.StatusInternalServerError, "tx_error")
return
}
defer tx.Rollback()
stmt, _ := tx.Prepare(`INSERT INTO pilots(competition_id,number,last_name,first_name,country,balloon_id)
VALUES(?,?,?,?,?,?)
ON CONFLICT(competition_id,number) DO UPDATE SET
last_name=excluded.last_name, first_name=excluded.first_name,
country=excluded.country, balloon_id=excluded.balloon_id`)
defer stmt.Close()
count := 0
for i, rec := range records {
if i == 0 {
lower := strings.ToLower(strings.TrimSpace(rec[0]))
if lower == "number" || lower == "nummer" || lower == "no" {
continue
}
}
if len(rec) < 3 {
continue
}
number := strings.TrimSpace(rec[0])
lastName := strings.TrimSpace(rec[1])
firstName := strings.TrimSpace(rec[2])
country := ""
balloon := ""
if len(rec) >= 4 {
country = strings.TrimSpace(rec[3])
}
if len(rec) >= 5 {
balloon = strings.TrimSpace(rec[4])
}
if number == "" {
continue
}
if _, err := stmt.Exec(id, number, lastName, firstName, country, balloon); err == nil {
count++
}
}
if err := tx.Commit(); err != nil {
writeError(w, http.StatusInternalServerError, "commit_error")
return
}
hub.broadcast(id, "pilot_changed", nil)
writeJSON(w, http.StatusOK, map[string]int{"imported": count})
}