package handlers import ( "crypto/rand" "encoding/base64" "encoding/json" "net/http" "golang.org/x/crypto/bcrypt" "xorm.io/xorm" "github.com/forgeo/forgebucket/internal/api/middleware" "github.com/forgeo/forgebucket/internal/models" ) type RunnerHandler struct{ db *xorm.Engine } func NewRunnerHandler(db *xorm.Engine) *RunnerHandler { return &RunnerHandler{db: db} } // List returns all registered runners. Admin-only. func (h *RunnerHandler) List(w http.ResponseWriter, r *http.Request) { if !isAdmin(r) { jsonError(w, "admin access required", http.StatusForbidden) return } var runners []models.Runner if err := h.db.Find(&runners); err != nil { jsonError(w, "could not list runners", http.StatusInternalServerError) return } if runners == nil { runners = []models.Runner{} } jsonOK(w, runners) } // Register creates a new runner record and returns the plaintext registration token // (shown once; the server stores only the bcrypt hash). func (h *RunnerHandler) Register(w http.ResponseWriter, r *http.Request) { if !isAdmin(r) { jsonError(w, "admin access required", http.StatusForbidden) return } var body struct { Name string `json:"name"` Labels []string `json:"labels"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { jsonError(w, "invalid request body", http.StatusBadRequest) return } if body.Name == "" { jsonError(w, "name is required", http.StatusBadRequest) return } raw := make([]byte, 32) if _, err := rand.Read(raw); err != nil { jsonError(w, "could not generate token", http.StatusInternalServerError) return } token := base64.RawURLEncoding.EncodeToString(raw) hash, err := bcrypt.GenerateFromPassword([]byte(token), bcrypt.DefaultCost) if err != nil { jsonError(w, "could not hash token", http.StatusInternalServerError) return } labelsJSON, _ := json.Marshal(body.Labels) runner := &models.Runner{ Name: body.Name, Labels: string(labelsJSON), Status: "idle", TokenHash: string(hash), } if _, err := h.db.Insert(runner); err != nil { jsonError(w, "runner name already taken", http.StatusConflict) return } w.WriteHeader(http.StatusCreated) jsonOK(w, map[string]any{ "id": runner.ID, "name": runner.Name, "token": token, // shown once — store it securely }) } func isAdmin(r *http.Request) bool { v, _ := r.Context().Value(middleware.ContextKeyIsAdmin).(bool) return v }