Files
PenaltyTracker/competitions.go
T

272 lines
8.1 KiB
Go

package main
import (
"database/sql"
"encoding/json"
"net/http"
"strconv"
)
func registerCompetitionRoutes(mux *http.ServeMux) {
mux.HandleFunc("GET /api/competitions", requireAuth(handleListCompetitions))
mux.HandleFunc("POST /api/competitions", requireAdmin(handleCreateCompetition))
mux.HandleFunc("GET /api/competitions/{id}", requireAuth(handleGetCompetition))
mux.HandleFunc("PATCH /api/competitions/{id}", requireAuth(handleUpdateCompetition))
mux.HandleFunc("DELETE /api/competitions/{id}", requireAdmin(handleDeleteCompetition))
mux.HandleFunc("GET /api/competitions/{id}/members", requireAuth(handleListMembers))
mux.HandleFunc("POST /api/competitions/{id}/members", requireAuth(handleAddMember))
mux.HandleFunc("DELETE /api/competitions/{id}/members/{uid}", requireAuth(handleRemoveMember))
}
func userRole(competitionID, userID int64) (string, error) {
var role string
err := db.QueryRow("SELECT role FROM competition_users WHERE competition_id=? AND user_id=?", competitionID, userID).Scan(&role)
if err == sql.ErrNoRows {
return "", nil
}
return role, err
}
func canAccessCompetition(u *User, competitionID int64) (string, bool) {
if u.IsSystemAdmin {
return "system_admin", true
}
role, err := userRole(competitionID, u.ID)
if err != nil || role == "" {
return "", false
}
return role, true
}
func handleListCompetitions(w http.ResponseWriter, r *http.Request) {
u := userFromCtx(r)
var rows *sql.Rows
var err error
if u.IsSystemAdmin {
rows, err = db.Query("SELECT id,name,allow_any_scorer_edit,rules_language,created_at FROM competitions ORDER BY created_at DESC")
} else {
rows, err = db.Query(`SELECT c.id,c.name,c.allow_any_scorer_edit,c.rules_language,c.created_at,cu.role
FROM competitions c JOIN competition_users cu ON cu.competition_id=c.id
WHERE cu.user_id=? ORDER BY c.created_at DESC`, u.ID)
}
if err != nil {
writeError(w, http.StatusInternalServerError, "db_error")
return
}
defer rows.Close()
out := []Competition{}
for rows.Next() {
var c Competition
var allow int
if u.IsSystemAdmin {
rows.Scan(&c.ID, &c.Name, &allow, &c.RulesLanguage, &c.CreatedAt)
c.Role = "system_admin"
} else {
rows.Scan(&c.ID, &c.Name, &allow, &c.RulesLanguage, &c.CreatedAt, &c.Role)
}
c.AllowAnyScorerEdit = allow == 1
out = append(out, c)
}
writeJSON(w, http.StatusOK, out)
}
func handleCreateCompetition(w http.ResponseWriter, r *http.Request) {
var req struct {
Name string `json:"name"`
AllowAnyScorerEdit bool `json:"allow_any_scorer_edit"`
RulesLanguage string `json:"rules_language"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid_body")
return
}
if req.Name == "" {
writeError(w, http.StatusBadRequest, "missing_name")
return
}
if len(req.Name) > 200 {
writeError(w, http.StatusBadRequest, "too_long")
return
}
allow := 0
if req.AllowAnyScorerEdit {
allow = 1
}
lang := normalizeRulesLanguage(req.RulesLanguage)
res, err := db.Exec("INSERT INTO competitions(name,allow_any_scorer_edit,rules_language) VALUES(?,?,?)", req.Name, allow, lang)
if err != nil {
writeError(w, http.StatusInternalServerError, "db_error")
return
}
id, _ := res.LastInsertId()
c := Competition{ID: id, Name: req.Name, AllowAnyScorerEdit: req.AllowAnyScorerEdit, RulesLanguage: lang}
writeJSON(w, http.StatusCreated, c)
}
func normalizeRulesLanguage(lang string) string {
if lang == "" {
return "en"
}
rulesMu.RLock()
_, ok := rules[lang]
rulesMu.RUnlock()
if !ok {
return "en"
}
return lang
}
func handleGetCompetition(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 {
writeError(w, http.StatusForbidden, "forbidden")
return
}
var c Competition
var allow int
err = db.QueryRow("SELECT id,name,allow_any_scorer_edit,rules_language,created_at FROM competitions WHERE id=?", id).
Scan(&c.ID, &c.Name, &allow, &c.RulesLanguage, &c.CreatedAt)
if err != nil {
writeError(w, http.StatusNotFound, "not_found")
return
}
c.AllowAnyScorerEdit = allow == 1
c.Role = role
writeJSON(w, http.StatusOK, c)
}
func handleUpdateCompetition(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 != "system_admin" && role != "chief_scorer") {
writeError(w, http.StatusForbidden, "forbidden")
return
}
var req struct {
Name *string `json:"name"`
AllowAnyScorerEdit *bool `json:"allow_any_scorer_edit"`
RulesLanguage *string `json:"rules_language"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid_body")
return
}
if req.Name != nil {
db.Exec("UPDATE competitions SET name=? WHERE id=?", *req.Name, id)
}
if req.AllowAnyScorerEdit != nil {
v := 0
if *req.AllowAnyScorerEdit {
v = 1
}
db.Exec("UPDATE competitions SET allow_any_scorer_edit=? WHERE id=?", v, id)
}
if req.RulesLanguage != nil {
db.Exec("UPDATE competitions SET rules_language=? WHERE id=?", normalizeRulesLanguage(*req.RulesLanguage), id)
}
hub.broadcast(id, "competition_updated", nil)
w.WriteHeader(http.StatusNoContent)
}
func handleDeleteCompetition(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
}
db.Exec("DELETE FROM competitions WHERE id=?", id)
w.WriteHeader(http.StatusNoContent)
}
func handleListMembers(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 u.id,u.username,u.display_name,cu.role
FROM competition_users cu JOIN users u ON u.id=cu.user_id
WHERE cu.competition_id=? ORDER BY cu.role,u.username`, id)
if err != nil {
writeError(w, http.StatusInternalServerError, "db_error")
return
}
defer rows.Close()
out := []CompetitionUser{}
for rows.Next() {
var m CompetitionUser
rows.Scan(&m.UserID, &m.Username, &m.DisplayName, &m.Role)
out = append(out, m)
}
writeJSON(w, http.StatusOK, out)
}
func handleAddMember(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 != "system_admin" && role != "chief_scorer") {
writeError(w, http.StatusForbidden, "forbidden")
return
}
var req struct {
UserID int64 `json:"user_id"`
Role string `json:"role"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid_body")
return
}
if req.Role != "chief_scorer" && req.Role != "scorer" {
writeError(w, http.StatusBadRequest, "invalid_role")
return
}
_, err = db.Exec("INSERT OR REPLACE INTO competition_users(competition_id,user_id,role) VALUES(?,?,?)", id, req.UserID, req.Role)
if err != nil {
writeError(w, http.StatusInternalServerError, "db_error")
return
}
w.WriteHeader(http.StatusNoContent)
}
func handleRemoveMember(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
}
uid, err := strconv.ParseInt(r.PathValue("uid"), 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid_id")
return
}
u := userFromCtx(r)
role, ok := canAccessCompetition(u, id)
if !ok || (role != "system_admin" && role != "chief_scorer") {
writeError(w, http.StatusForbidden, "forbidden")
return
}
db.Exec("DELETE FROM competition_users WHERE competition_id=? AND user_id=?", id, uid)
w.WriteHeader(http.StatusNoContent)
}