package main import ( "encoding/json" "net/http" "strconv" "golang.org/x/crypto/bcrypt" ) func registerUserRoutes(mux *http.ServeMux) { mux.HandleFunc("GET /api/users", requireAdmin(handleListUsers)) mux.HandleFunc("POST /api/users", requireAuth(handleCreateUser)) mux.HandleFunc("DELETE /api/users/{id}", requireAdmin(handleDeleteUser)) mux.HandleFunc("PATCH /api/users/{id}", requireAdmin(handleAdminUpdateUser)) } func handleListUsers(w http.ResponseWriter, r *http.Request) { rows, err := db.Query("SELECT id,username,display_name,language,is_system_admin,must_change_password FROM users ORDER BY username") if err != nil { writeError(w, http.StatusInternalServerError, "db_error") return } defer rows.Close() out := []User{} for rows.Next() { var u User var admin, mustChange int rows.Scan(&u.ID, &u.Username, &u.DisplayName, &u.Language, &admin, &mustChange) u.IsSystemAdmin = admin == 1 u.MustChangePassword = mustChange == 1 out = append(out, u) } writeJSON(w, http.StatusOK, out) } func handleCreateUser(w http.ResponseWriter, r *http.Request) { actor := userFromCtx(r) var req struct { Username string `json:"username"` Password string `json:"password"` DisplayName string `json:"display_name"` Language string `json:"language"` IsSystemAdmin bool `json:"is_system_admin"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid_body") return } req.Username = normalizeUsername(req.Username) if req.Username == "" || req.Password == "" { writeError(w, http.StatusBadRequest, "missing_fields") return } if len(req.Username) > maxUsernameLen || len(req.DisplayName) > maxDisplayNameLen { writeError(w, http.StatusBadRequest, "too_long") return } if msg := validatePassword(req.Password); msg != "" { writeError(w, http.StatusBadRequest, msg) return } if req.IsSystemAdmin && !actor.IsSystemAdmin { writeError(w, http.StatusForbidden, "forbidden") return } if !actor.IsSystemAdmin { var chiefCount int db.QueryRow("SELECT COUNT(*) FROM competition_users WHERE user_id=? AND role='chief_scorer'", actor.ID).Scan(&chiefCount) if chiefCount == 0 { writeError(w, http.StatusForbidden, "forbidden") return } } hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { writeError(w, http.StatusInternalServerError, "hash_error") return } if req.Language == "" { req.Language = "en" } admin := 0 if req.IsSystemAdmin { admin = 1 } res, err := db.Exec( "INSERT INTO users(username,password_hash,display_name,language,is_system_admin) VALUES(?,?,?,?,?)", req.Username, string(hash), req.DisplayName, req.Language, admin, ) if err != nil { writeError(w, http.StatusConflict, "username_taken") return } id, _ := res.LastInsertId() u, _ := loadUser(id) writeJSON(w, http.StatusCreated, u) } func handleDeleteUser(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 } actor := userFromCtx(r) if actor.ID == id { writeError(w, http.StatusBadRequest, "cannot_delete_self") return } if _, err := db.Exec("DELETE FROM users WHERE id=?", id); err != nil { writeError(w, http.StatusInternalServerError, "db_error") return } w.WriteHeader(http.StatusNoContent) } func handleAdminUpdateUser(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 } var req struct { Username *string `json:"username"` DisplayName *string `json:"display_name"` Password *string `json:"password"` IsSystemAdmin *bool `json:"is_system_admin"` MustChangePassword *bool `json:"must_change_password"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid_body") return } if req.Username != nil { newName := normalizeUsername(*req.Username) if newName == "" || len(newName) > maxUsernameLen { writeError(w, http.StatusBadRequest, "invalid_username") return } if _, err := db.Exec("UPDATE users SET username=? WHERE id=?", newName, id); err != nil { writeError(w, http.StatusConflict, "username_taken") return } } if req.DisplayName != nil { if len(*req.DisplayName) > maxDisplayNameLen { writeError(w, http.StatusBadRequest, "too_long") return } db.Exec("UPDATE users SET display_name=? WHERE id=?", *req.DisplayName, id) } if req.Password != nil && *req.Password != "" { if msg := validatePassword(*req.Password); msg != "" { writeError(w, http.StatusBadRequest, msg) return } hash, _ := bcrypt.GenerateFromPassword([]byte(*req.Password), bcrypt.DefaultCost) db.Exec("UPDATE users SET password_hash=? WHERE id=?", string(hash), id) } if req.IsSystemAdmin != nil { v := 0 if *req.IsSystemAdmin { v = 1 } db.Exec("UPDATE users SET is_system_admin=? WHERE id=?", v, id) } if req.MustChangePassword != nil { v := 0 if *req.MustChangePassword { v = 1 } db.Exec("UPDATE users SET must_change_password=? WHERE id=?", v, id) } u, _ := loadUser(id) writeJSON(w, http.StatusOK, u) }