Replaced one-pager with multiple pages and fixed security bugs
This commit is contained in:
+99
-8
@@ -9,12 +9,91 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// csvSafe prefixes potentially dangerous CSV cell content with a single quote
|
||||
// so that spreadsheet apps don't interpret it as a formula.
|
||||
func csvSafe(s string) string {
|
||||
if s == "" {
|
||||
return s
|
||||
}
|
||||
switch s[0] {
|
||||
case '=', '+', '-', '@', '\t', '\r':
|
||||
return "'" + s
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func clip(s string, max int) string {
|
||||
if len(s) > max {
|
||||
return s[:max]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func registerPenaltyRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /api/competitions/{id}/penalties", requireAuth(handleListPenalties))
|
||||
mux.HandleFunc("POST /api/competitions/{id}/penalties", requireAuth(handleCreatePenalty))
|
||||
mux.HandleFunc("PATCH /api/competitions/{id}/penalties/{pid}", requireAuth(handleUpdatePenalty))
|
||||
mux.HandleFunc("DELETE /api/competitions/{id}/penalties/{pid}", requireAuth(handleDeletePenalty))
|
||||
mux.HandleFunc("GET /api/competitions/{id}/penalties.csv", requireAuth(handleExportPenalties))
|
||||
mux.HandleFunc("POST /api/competitions/{id}/penalties/apply", requireAuth(handleApplyPenalties))
|
||||
}
|
||||
|
||||
// handleApplyPenalties bulk-marks penalties as applied (transferred=1) for a
|
||||
// given task. Requires chief_scorer or system_admin role.
|
||||
func handleApplyPenalties(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)
|
||||
role, ok := canAccessCompetition(u, id)
|
||||
if !ok || (role != "chief_scorer" && role != "system_admin") {
|
||||
writeError(w, http.StatusForbidden, "forbidden")
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Task string `json:"task"`
|
||||
IDs []int64 `json:"ids"`
|
||||
Applied bool `json:"applied"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid_body")
|
||||
return
|
||||
}
|
||||
v := 0
|
||||
if req.Applied {
|
||||
v = 1
|
||||
}
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "tx_error")
|
||||
return
|
||||
}
|
||||
defer tx.Rollback()
|
||||
if len(req.IDs) > 0 {
|
||||
stmt, err := tx.Prepare("UPDATE penalties SET transferred=?, updated_at=datetime('now') WHERE id=? AND competition_id=?")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "db_error")
|
||||
return
|
||||
}
|
||||
defer stmt.Close()
|
||||
for _, pid := range req.IDs {
|
||||
stmt.Exec(v, pid, id)
|
||||
}
|
||||
} else {
|
||||
task := clip(req.Task, 64)
|
||||
if _, err := tx.Exec("UPDATE penalties SET transferred=?, updated_at=datetime('now') WHERE competition_id=? AND task=?", v, id, task); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "db_error")
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "commit_error")
|
||||
return
|
||||
}
|
||||
hub.broadcast(id, "penalties_applied", map[string]any{"task": req.Task, "applied": req.Applied, "ids": req.IDs})
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func loadPenalty(id int64) (*Penalty, error) {
|
||||
@@ -92,6 +171,13 @@ func handleCreatePenalty(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusBadRequest, "invalid_body")
|
||||
return
|
||||
}
|
||||
pen.Flight = clip(pen.Flight, 64)
|
||||
pen.Date = clip(pen.Date, 32)
|
||||
pen.PilotNumber = clip(pen.PilotNumber, 32)
|
||||
pen.RuleNumber = clip(pen.RuleNumber, 64)
|
||||
pen.Task = clip(pen.Task, 64)
|
||||
pen.PenaltiesText = clip(pen.PenaltiesText, 256)
|
||||
pen.Description = clip(pen.Description, maxFieldLen)
|
||||
transferred := 0
|
||||
if pen.Transferred {
|
||||
transferred = 1
|
||||
@@ -171,31 +257,31 @@ func handleUpdatePenalty(w http.ResponseWriter, r *http.Request) {
|
||||
args := []any{}
|
||||
if req.Flight != nil {
|
||||
sets = append(sets, "flight=?")
|
||||
args = append(args, *req.Flight)
|
||||
args = append(args, clip(*req.Flight, 64))
|
||||
}
|
||||
if req.Date != nil {
|
||||
sets = append(sets, "date=?")
|
||||
args = append(args, *req.Date)
|
||||
args = append(args, clip(*req.Date, 32))
|
||||
}
|
||||
if req.PilotNumber != nil {
|
||||
sets = append(sets, "pilot_number=?")
|
||||
args = append(args, *req.PilotNumber)
|
||||
args = append(args, clip(*req.PilotNumber, 32))
|
||||
}
|
||||
if req.RuleNumber != nil {
|
||||
sets = append(sets, "rule_number=?")
|
||||
args = append(args, *req.RuleNumber)
|
||||
args = append(args, clip(*req.RuleNumber, 64))
|
||||
}
|
||||
if req.Task != nil {
|
||||
sets = append(sets, "task=?")
|
||||
args = append(args, *req.Task)
|
||||
args = append(args, clip(*req.Task, 64))
|
||||
}
|
||||
if req.PenaltiesText != nil {
|
||||
sets = append(sets, "penalties_text=?")
|
||||
args = append(args, *req.PenaltiesText)
|
||||
args = append(args, clip(*req.PenaltiesText, 256))
|
||||
}
|
||||
if req.Description != nil {
|
||||
sets = append(sets, "description=?")
|
||||
args = append(args, *req.Description)
|
||||
args = append(args, clip(*req.Description, maxFieldLen))
|
||||
}
|
||||
if req.Transferred != nil {
|
||||
v := 0
|
||||
@@ -287,7 +373,12 @@ func handleExportPenalties(w http.ResponseWriter, r *http.Request) {
|
||||
if transferred == 1 {
|
||||
t = "1"
|
||||
}
|
||||
cw.Write([]string{strconv.FormatInt(idv, 10), flight, date, pnum, pname, rnum, task, pens, desc, creator, t, createdAt})
|
||||
cw.Write([]string{
|
||||
strconv.FormatInt(idv, 10),
|
||||
csvSafe(flight), csvSafe(date), csvSafe(pnum), csvSafe(pname),
|
||||
csvSafe(rnum), csvSafe(task), csvSafe(pens), csvSafe(desc),
|
||||
csvSafe(creator), t, createdAt,
|
||||
})
|
||||
}
|
||||
cw.Flush()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user