Add basic web app and API integration
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
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
|
||||
}
|
||||
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})
|
||||
}
|
||||
Reference in New Issue
Block a user