package main import ( "errors" "fmt" "io" "math/rand" "net/http" "strconv" "strings" "time" "github.com/labstack/echo/v4" ) func (a *App) handleHealth(c echo.Context) error { return c.JSON(http.StatusOK, map[string]string{"status": "ok"}) } func (a *App) handleGetState(c echo.Context) error { state, err := a.readState(a.isAdminRequest(c)) if err != nil { return writeError(c, http.StatusInternalServerError, err.Error()) } return c.JSON(http.StatusOK, state) } func (a *App) handleAdminLogin(c echo.Context) error { var req AdminLoginRequest if err := c.Bind(&req); err != nil { return writeError(c, http.StatusBadRequest, "invalid request body") } if !a.verifyAdmin(req.Username, req.Password) { return writeError(c, http.StatusUnauthorized, "invalid credentials") } token, expiry, err := a.sessions.CreateToken() if err != nil { return writeError(c, http.StatusInternalServerError, "failed to create session") } return c.JSON(http.StatusOK, map[string]any{ "token": token, "expiresAt": expiry.Format(time.RFC3339), "username": a.cfg.AdminUser, }) } func (a *App) handleAdminLogout(c echo.Context) error { token := strings.TrimSpace(strings.TrimPrefix(c.Request().Header.Get(echo.HeaderAuthorization), "Bearer ")) a.sessions.DeleteToken(token) return c.JSON(http.StatusOK, map[string]bool{"ok": true}) } func (a *App) handleUpdateAdminSettings(c echo.Context) error { var req AdminSettingsUpdateRequest if err := c.Bind(&req); err != nil { return writeError(c, http.StatusBadRequest, "invalid request body") } if req.ViewProofInView == nil { return writeError(c, http.StatusBadRequest, "viewProofInView is required") } if err := a.updateViewProofInView(*req.ViewProofInView); err != nil { return writeError(c, http.StatusInternalServerError, err.Error()) } a.events.Broadcast() state, err := a.readState(true) if err != nil { return writeError(c, http.StatusInternalServerError, err.Error()) } return c.JSON(http.StatusOK, state) } func (a *App) handleCreatePlayer(c echo.Context) error { var req PlayerCreateRequest if err := c.Bind(&req); err != nil { return writeError(c, http.StatusBadRequest, "invalid request body") } nameAr := strings.TrimSpace(req.NameAr) nameEn := strings.TrimSpace(req.NameEn) group := normalizeGroup(req.GroupCode) if nameAr == "" || nameEn == "" { return writeError(c, http.StatusBadRequest, "both Arabic and English names are required") } tx, err := a.db.Begin() if err != nil { return writeError(c, http.StatusInternalServerError, "failed to start transaction") } defer tx.Rollback() res, err := tx.Exec(` INSERT INTO players(name_ar, name_en, group_code, image_data) VALUES(?, ?, ?, '') `, nameAr, nameEn, group) if err != nil { return writeError(c, http.StatusInternalServerError, fmt.Sprintf("create player: %v", err)) } id64, err := res.LastInsertId() if err != nil { return writeError(c, http.StatusInternalServerError, "failed to read new player id") } playerID := int(id64) for _, stage := range scoreStages { if _, err := tx.Exec(`INSERT OR IGNORE INTO scores(stage, player_id, score) VALUES(?, ?, 0)`, stage, playerID); err != nil { return writeError(c, http.StatusInternalServerError, fmt.Sprintf("create score row: %v", err)) } } if err := tx.Commit(); err != nil { return writeError(c, http.StatusInternalServerError, "failed to commit create player") } a.events.Broadcast() state, err := a.readState(true) if err != nil { return writeError(c, http.StatusInternalServerError, err.Error()) } return c.JSON(http.StatusCreated, state) } func (a *App) handleUpdatePlayer(c echo.Context) error { playerID, err := strconv.Atoi(c.Param("id")) if err != nil { return writeError(c, http.StatusBadRequest, "invalid player id") } var req PlayerUpdateRequest if err := c.Bind(&req); err != nil { return writeError(c, http.StatusBadRequest, "invalid request body") } updates := []string{} args := []any{} if req.NameAr != nil { nameAr := strings.TrimSpace(*req.NameAr) if nameAr == "" { return writeError(c, http.StatusBadRequest, "arabic name cannot be empty") } updates = append(updates, "name_ar = ?") args = append(args, nameAr) } if req.NameEn != nil { nameEn := strings.TrimSpace(*req.NameEn) if nameEn == "" { return writeError(c, http.StatusBadRequest, "english name cannot be empty") } updates = append(updates, "name_en = ?") args = append(args, nameEn) } if req.GroupCode != nil { updates = append(updates, "group_code = ?") args = append(args, normalizeGroup(*req.GroupCode)) } if req.ImageData != nil { img := strings.TrimSpace(*req.ImageData) if len(img) > 2_500_000 { return writeError(c, http.StatusBadRequest, "image payload too large") } updates = append(updates, "image_data = ?") args = append(args, img) } if req.ClearImage { updates = append(updates, "image_data = ''") } if len(updates) == 0 { return writeError(c, http.StatusBadRequest, "no fields to update") } updates = append(updates, "updated_at = CURRENT_TIMESTAMP") args = append(args, playerID) query := fmt.Sprintf("UPDATE players SET %s WHERE id = ?", strings.Join(updates, ", ")) res, err := a.db.Exec(query, args...) if err != nil { return writeError(c, http.StatusInternalServerError, fmt.Sprintf("update player: %v", err)) } affected, err := res.RowsAffected() if err != nil { return writeError(c, http.StatusInternalServerError, "failed to check update result") } if affected == 0 { return writeError(c, http.StatusNotFound, "player not found") } a.events.Broadcast() state, err := a.readState(true) if err != nil { return writeError(c, http.StatusInternalServerError, err.Error()) } return c.JSON(http.StatusOK, state) } func (a *App) handleDeletePlayer(c echo.Context) error { playerID, err := strconv.Atoi(c.Param("id")) if err != nil { return writeError(c, http.StatusBadRequest, "invalid player id") } res, err := a.db.Exec(`DELETE FROM players WHERE id = ?`, playerID) if err != nil { return writeError(c, http.StatusInternalServerError, fmt.Sprintf("delete player: %v", err)) } affected, err := res.RowsAffected() if err != nil { return writeError(c, http.StatusInternalServerError, "failed to check delete result") } if affected == 0 { return writeError(c, http.StatusNotFound, "player not found") } a.events.Broadcast() state, err := a.readState(true) if err != nil { return writeError(c, http.StatusInternalServerError, err.Error()) } return c.JSON(http.StatusOK, state) } func (a *App) handleAutoGroupPlayers(c echo.Context) error { var req AutoGroupPlayersRequest if err := c.Bind(&req); err != nil { return writeError(c, http.StatusBadRequest, "invalid request body") } groups := normalizeGroups(req.Groups) if len(groups) == 0 { return writeError(c, http.StatusBadRequest, "at least one group is required") } rows, err := a.db.Query(`SELECT id FROM players ORDER BY id ASC`) if err != nil { return writeError(c, http.StatusInternalServerError, fmt.Sprintf("query players: %v", err)) } defer rows.Close() playerIDs := []int{} for rows.Next() { var id int if err := rows.Scan(&id); err != nil { return writeError(c, http.StatusInternalServerError, fmt.Sprintf("scan player: %v", err)) } playerIDs = append(playerIDs, id) } if err := rows.Err(); err != nil { return writeError(c, http.StatusInternalServerError, fmt.Sprintf("read players: %v", err)) } rng := rand.New(rand.NewSource(time.Now().UnixNano())) rng.Shuffle(len(playerIDs), func(i, j int) { playerIDs[i], playerIDs[j] = playerIDs[j], playerIDs[i] }) tx, err := a.db.Begin() if err != nil { return writeError(c, http.StatusInternalServerError, "failed to start transaction") } defer tx.Rollback() for i, playerID := range playerIDs { groupCode := groups[i%len(groups)] if _, err := tx.Exec(`UPDATE players SET group_code = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, groupCode, playerID); err != nil { return writeError(c, http.StatusInternalServerError, fmt.Sprintf("assign player group: %v", err)) } } if err := tx.Commit(); err != nil { return writeError(c, http.StatusInternalServerError, "failed to commit group assignment") } a.events.Broadcast() state, err := a.readState(true) if err != nil { return writeError(c, http.StatusInternalServerError, err.Error()) } return c.JSON(http.StatusOK, state) } func (a *App) handleUpdateScore(c echo.Context) error { stage, err := validateStage(c.Param("stage")) if err != nil { return writeError(c, http.StatusBadRequest, err.Error()) } playerID, err := strconv.Atoi(c.Param("id")) if err != nil { return writeError(c, http.StatusBadRequest, "invalid player id") } var req ScoreUpdateRequest if err := c.Bind(&req); err != nil { return writeError(c, http.StatusBadRequest, "invalid request body") } if req.Score < 0 || req.Score > 9999 { return writeError(c, http.StatusBadRequest, "score must be between 0 and 9999") } res, err := a.db.Exec(` INSERT INTO scores(stage, player_id, score) VALUES(?, ?, ?) ON CONFLICT(stage, player_id) DO UPDATE SET score = excluded.score, updated_at = CURRENT_TIMESTAMP `, stage, playerID, req.Score) if err != nil { return writeError(c, http.StatusInternalServerError, fmt.Sprintf("update score: %v", err)) } if _, err := res.RowsAffected(); err != nil { return writeError(c, http.StatusInternalServerError, "failed to check score update") } a.events.Broadcast() state, err := a.readState(true) if err != nil { return writeError(c, http.StatusInternalServerError, err.Error()) } return c.JSON(http.StatusOK, state) } func (a *App) handleResetStageScores(c echo.Context) error { stage, err := validateStage(c.Param("stage")) if err != nil { return writeError(c, http.StatusBadRequest, err.Error()) } var req ResetStageRequest if err := c.Bind(&req); err != nil && !errors.Is(err, io.EOF) { return writeError(c, http.StatusBadRequest, "invalid request body") } if _, err := a.db.Exec(`UPDATE scores SET score = 0, updated_at = CURRENT_TIMESTAMP WHERE stage = ?`, stage); err != nil { return writeError(c, http.StatusInternalServerError, fmt.Sprintf("reset stage scores: %v", err)) } if req.ResetProofs { if _, err := a.db.Exec(`DELETE FROM score_attachments WHERE stage = ?`, stage); err != nil { return writeError(c, http.StatusInternalServerError, fmt.Sprintf("reset stage proofs: %v", err)) } } a.events.Broadcast() state, err := a.readState(true) if err != nil { return writeError(c, http.StatusInternalServerError, err.Error()) } return c.JSON(http.StatusOK, state) } func (a *App) handleUpdateScoreProof(c echo.Context) error { stage, err := validateStage(c.Param("stage")) if err != nil { return writeError(c, http.StatusBadRequest, err.Error()) } playerID, err := strconv.Atoi(c.Param("id")) if err != nil { return writeError(c, http.StatusBadRequest, "invalid player id") } var req ScoreProofUpdateRequest if err := c.Bind(&req); err != nil { return writeError(c, http.StatusBadRequest, "invalid request body") } imageData := strings.TrimSpace(req.ImageData) if imageData == "" { return writeError(c, http.StatusBadRequest, "imageData is required") } if len(imageData) > 5_000_000 { return writeError(c, http.StatusBadRequest, "image payload too large") } if _, err := a.db.Exec(` INSERT INTO score_attachments(stage, player_id, image_data) VALUES(?, ?, ?) ON CONFLICT(stage, player_id) DO UPDATE SET image_data = excluded.image_data, updated_at = CURRENT_TIMESTAMP `, stage, playerID, imageData); err != nil { return writeError(c, http.StatusInternalServerError, fmt.Sprintf("update score proof: %v", err)) } a.events.Broadcast() state, err := a.readState(true) if err != nil { return writeError(c, http.StatusInternalServerError, err.Error()) } return c.JSON(http.StatusOK, state) } func (a *App) handleDeleteScoreProof(c echo.Context) error { stage, err := validateStage(c.Param("stage")) if err != nil { return writeError(c, http.StatusBadRequest, err.Error()) } playerID, err := strconv.Atoi(c.Param("id")) if err != nil { return writeError(c, http.StatusBadRequest, "invalid player id") } if _, err := a.db.Exec(`DELETE FROM score_attachments WHERE stage = ? AND player_id = ?`, stage, playerID); err != nil { return writeError(c, http.StatusInternalServerError, fmt.Sprintf("delete score proof: %v", err)) } a.events.Broadcast() state, err := a.readState(true) if err != nil { return writeError(c, http.StatusInternalServerError, err.Error()) } return c.JSON(http.StatusOK, state) } func validateStage(stage string) (string, error) { normalized := strings.ToLower(strings.TrimSpace(stage)) for _, allowed := range scoreStages { if normalized == allowed { return normalized, nil } } return "", fmt.Errorf("invalid stage") } func normalizeGroup(group string) string { return strings.TrimSpace(group) } func normalizeGroups(groups []string) []string { seen := map[string]bool{} out := []string{} for _, group := range groups { normalized := normalizeGroup(group) if normalized == "" || seen[normalized] { continue } seen[normalized] = true out = append(out, normalized) } return out } func writeError(c echo.Context, status int, message string) error { return c.JSON(status, map[string]string{"message": message}) }