package api import ( "database/sql" "encoding/json" "fmt" "net/http" "time" "jiggablend/pkg/types" ) // handleGenerateRunnerAPIKey generates a new runner API key func (s *Server) handleGenerateRunnerAPIKey(w http.ResponseWriter, r *http.Request) { userID, err := getUserID(r) if err != nil { s.respondError(w, http.StatusUnauthorized, err.Error()) return } var req struct { Name string `json:"name"` Description string `json:"description,omitempty"` Scope string `json:"scope,omitempty"` // 'manager' or 'user' } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { s.respondError(w, http.StatusBadRequest, fmt.Sprintf("Invalid request body: expected valid JSON - %v", err)) return } if req.Name == "" { s.respondError(w, http.StatusBadRequest, "API key name is required") return } // Default scope to 'user' if not specified scope := req.Scope if scope == "" { scope = "user" } if scope != "manager" && scope != "user" { s.respondError(w, http.StatusBadRequest, "Scope must be 'manager' or 'user'") return } keyInfo, err := s.secrets.GenerateRunnerAPIKey(userID, req.Name, req.Description, scope) if err != nil { s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to generate API key: %v", err)) return } response := map[string]interface{}{ "id": keyInfo.ID, "key": keyInfo.Key, "name": keyInfo.Name, "description": keyInfo.Description, "is_active": keyInfo.IsActive, "created_at": keyInfo.CreatedAt, } s.respondJSON(w, http.StatusCreated, response) } // handleListRunnerAPIKeys lists all runner API keys func (s *Server) handleListRunnerAPIKeys(w http.ResponseWriter, r *http.Request) { keys, err := s.secrets.ListRunnerAPIKeys() if err != nil { s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to list API keys: %v", err)) return } // Convert to response format (hide sensitive hash data) var response []map[string]interface{} for _, key := range keys { item := map[string]interface{}{ "id": key.ID, "key_prefix": key.Key, // Only show prefix, not full key "name": key.Name, "is_active": key.IsActive, "created_at": key.CreatedAt, "created_by": key.CreatedBy, } if key.Description != nil { item["description"] = *key.Description } response = append(response, item) } s.respondJSON(w, http.StatusOK, response) } // handleRevokeRunnerAPIKey revokes a runner API key func (s *Server) handleRevokeRunnerAPIKey(w http.ResponseWriter, r *http.Request) { keyID, err := parseID(r, "id") if err != nil { s.respondError(w, http.StatusBadRequest, err.Error()) return } if err := s.secrets.RevokeRunnerAPIKey(keyID); err != nil { s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to revoke API key: %v", err)) return } s.respondJSON(w, http.StatusOK, map[string]string{"message": "API key revoked"}) } // handleDeleteRunnerAPIKey deletes a runner API key func (s *Server) handleDeleteRunnerAPIKey(w http.ResponseWriter, r *http.Request) { keyID, err := parseID(r, "id") if err != nil { s.respondError(w, http.StatusBadRequest, err.Error()) return } if err := s.secrets.DeleteRunnerAPIKey(keyID); err != nil { s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to delete API key: %v", err)) return } s.respondJSON(w, http.StatusOK, map[string]string{"message": "API key deleted"}) } // handleVerifyRunner manually verifies a runner func (s *Server) handleVerifyRunner(w http.ResponseWriter, r *http.Request) { runnerID, err := parseID(r, "id") if err != nil { s.respondError(w, http.StatusBadRequest, err.Error()) return } // Check if runner exists var exists bool err = s.db.With(func(conn *sql.DB) error { return conn.QueryRow("SELECT EXISTS(SELECT 1 FROM runners WHERE id = ?)", runnerID).Scan(&exists) }) if err != nil || !exists { s.respondError(w, http.StatusNotFound, "Runner not found") return } // Mark runner as verified err = s.db.With(func(conn *sql.DB) error { _, err := conn.Exec("UPDATE runners SET verified = 1 WHERE id = ?", runnerID) return err }) if err != nil { s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to verify runner: %v", err)) return } s.respondJSON(w, http.StatusOK, map[string]string{"message": "Runner verified"}) } // handleDeleteRunner removes a runner func (s *Server) handleDeleteRunner(w http.ResponseWriter, r *http.Request) { runnerID, err := parseID(r, "id") if err != nil { s.respondError(w, http.StatusBadRequest, err.Error()) return } // Check if runner exists var exists bool err = s.db.With(func(conn *sql.DB) error { return conn.QueryRow("SELECT EXISTS(SELECT 1 FROM runners WHERE id = ?)", runnerID).Scan(&exists) }) if err != nil || !exists { s.respondError(w, http.StatusNotFound, "Runner not found") return } // Delete runner err = s.db.With(func(conn *sql.DB) error { _, err := conn.Exec("DELETE FROM runners WHERE id = ?", runnerID) return err }) if err != nil { s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to delete runner: %v", err)) return } s.respondJSON(w, http.StatusOK, map[string]string{"message": "Runner deleted"}) } // handleListRunnersAdmin lists all runners with admin details func (s *Server) handleListRunnersAdmin(w http.ResponseWriter, r *http.Request) { var rows *sql.Rows err := s.db.With(func(conn *sql.DB) error { var err error rows, err = conn.Query( `SELECT id, name, hostname, status, last_heartbeat, capabilities, api_key_id, api_key_scope, priority, created_at FROM runners ORDER BY created_at DESC`, ) return err }) if err != nil { s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to query runners: %v", err)) return } defer rows.Close() // Get the set of currently connected runners via WebSocket // This is the source of truth for online status s.runnerConnsMu.RLock() connectedRunners := make(map[int64]bool) for runnerID := range s.runnerConns { connectedRunners[runnerID] = true } s.runnerConnsMu.RUnlock() runners := []map[string]interface{}{} for rows.Next() { var runner types.Runner var apiKeyID sql.NullInt64 var apiKeyScope string err := rows.Scan( &runner.ID, &runner.Name, &runner.Hostname, &runner.Status, &runner.LastHeartbeat, &runner.Capabilities, &apiKeyID, &apiKeyScope, &runner.Priority, &runner.CreatedAt, ) if err != nil { s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to scan runner: %v", err)) return } // Override status based on actual WebSocket connection state // The WebSocket connection is the source of truth for runner status actualStatus := runner.Status if connectedRunners[runner.ID] { actualStatus = types.RunnerStatusOnline } else if runner.Status == types.RunnerStatusOnline { // Database says online but not connected via WebSocket - mark as offline actualStatus = types.RunnerStatusOffline } runners = append(runners, map[string]interface{}{ "id": runner.ID, "name": runner.Name, "hostname": runner.Hostname, "status": actualStatus, "last_heartbeat": runner.LastHeartbeat, "capabilities": runner.Capabilities, "api_key_id": apiKeyID.Int64, "api_key_scope": apiKeyScope, "priority": runner.Priority, "created_at": runner.CreatedAt, }) } s.respondJSON(w, http.StatusOK, runners) } // handleListUsers lists all users func (s *Server) handleListUsers(w http.ResponseWriter, r *http.Request) { // Get first user ID to mark it in the response firstUserID, err := s.auth.GetFirstUserID() if err != nil { // If no users exist, firstUserID will be 0, which is fine firstUserID = 0 } var rows *sql.Rows err = s.db.With(func(conn *sql.DB) error { var err error rows, err = conn.Query( `SELECT id, email, name, oauth_provider, is_admin, created_at FROM users ORDER BY created_at DESC`, ) return err }) if err != nil { s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to query users: %v", err)) return } defer rows.Close() users := []map[string]interface{}{} for rows.Next() { var userID int64 var email, name, oauthProvider string var isAdmin bool var createdAt time.Time err := rows.Scan(&userID, &email, &name, &oauthProvider, &isAdmin, &createdAt) if err != nil { s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to scan user: %v", err)) return } // Get job count for this user var jobCount int err = s.db.With(func(conn *sql.DB) error { return conn.QueryRow("SELECT COUNT(*) FROM jobs WHERE user_id = ?", userID).Scan(&jobCount) }) if err != nil { jobCount = 0 // Default to 0 if query fails } users = append(users, map[string]interface{}{ "id": userID, "email": email, "name": name, "oauth_provider": oauthProvider, "is_admin": isAdmin, "created_at": createdAt, "job_count": jobCount, "is_first_user": userID == firstUserID, }) } s.respondJSON(w, http.StatusOK, users) } // handleGetUserJobs gets all jobs for a specific user func (s *Server) handleGetUserJobs(w http.ResponseWriter, r *http.Request) { userID, err := parseID(r, "id") if err != nil { s.respondError(w, http.StatusBadRequest, err.Error()) return } // Verify user exists var exists bool err = s.db.With(func(conn *sql.DB) error { return conn.QueryRow("SELECT EXISTS(SELECT 1 FROM users WHERE id = ?)", userID).Scan(&exists) }) if err != nil || !exists { s.respondError(w, http.StatusNotFound, "User not found") return } var rows *sql.Rows err = s.db.With(func(conn *sql.DB) error { var err error rows, err = conn.Query( `SELECT id, user_id, job_type, name, status, progress, frame_start, frame_end, output_format, allow_parallel_runners, timeout_seconds, blend_metadata, created_at, started_at, completed_at, error_message FROM jobs WHERE user_id = ? ORDER BY created_at DESC`, userID, ) return err }) if err != nil { s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to query jobs: %v", err)) return } defer rows.Close() jobs := []types.Job{} for rows.Next() { var job types.Job var jobType string var startedAt, completedAt sql.NullTime var blendMetadataJSON sql.NullString var errorMessage sql.NullString var frameStart, frameEnd sql.NullInt64 var outputFormat sql.NullString var allowParallelRunners sql.NullBool err := rows.Scan( &job.ID, &job.UserID, &jobType, &job.Name, &job.Status, &job.Progress, &frameStart, &frameEnd, &outputFormat, &allowParallelRunners, &job.TimeoutSeconds, &blendMetadataJSON, &job.CreatedAt, &startedAt, &completedAt, &errorMessage, ) if err != nil { s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to scan job: %v", err)) return } job.JobType = types.JobType(jobType) if frameStart.Valid { fs := int(frameStart.Int64) job.FrameStart = &fs } if frameEnd.Valid { fe := int(frameEnd.Int64) job.FrameEnd = &fe } if outputFormat.Valid { job.OutputFormat = &outputFormat.String } if allowParallelRunners.Valid { job.AllowParallelRunners = &allowParallelRunners.Bool } if startedAt.Valid { job.StartedAt = &startedAt.Time } if completedAt.Valid { job.CompletedAt = &completedAt.Time } if blendMetadataJSON.Valid && blendMetadataJSON.String != "" { var metadata types.BlendMetadata if err := json.Unmarshal([]byte(blendMetadataJSON.String), &metadata); err == nil { job.BlendMetadata = &metadata } } if errorMessage.Valid { job.ErrorMessage = errorMessage.String } jobs = append(jobs, job) } s.respondJSON(w, http.StatusOK, jobs) } // handleGetRegistrationEnabled gets the registration enabled setting func (s *Server) handleGetRegistrationEnabled(w http.ResponseWriter, r *http.Request) { enabled, err := s.auth.IsRegistrationEnabled() if err != nil { s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get registration setting: %v", err)) return } s.respondJSON(w, http.StatusOK, map[string]bool{"enabled": enabled}) } // handleSetRegistrationEnabled sets the registration enabled setting func (s *Server) handleSetRegistrationEnabled(w http.ResponseWriter, r *http.Request) { var req struct { Enabled bool `json:"enabled"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { s.respondError(w, http.StatusBadRequest, fmt.Sprintf("Invalid request body: expected valid JSON - %v", err)) return } if err := s.auth.SetRegistrationEnabled(req.Enabled); err != nil { s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to set registration setting: %v", err)) return } s.respondJSON(w, http.StatusOK, map[string]bool{"enabled": req.Enabled}) } // handleSetUserAdminStatus sets a user's admin status (admin only) func (s *Server) handleSetUserAdminStatus(w http.ResponseWriter, r *http.Request) { targetUserID, err := parseID(r, "id") if err != nil { s.respondError(w, http.StatusBadRequest, err.Error()) return } var req struct { IsAdmin bool `json:"is_admin"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { s.respondError(w, http.StatusBadRequest, fmt.Sprintf("Invalid request body: expected valid JSON - %v", err)) return } if err := s.auth.SetUserAdminStatus(targetUserID, req.IsAdmin); err != nil { s.respondError(w, http.StatusBadRequest, err.Error()) return } s.respondJSON(w, http.StatusOK, map[string]interface{}{ "user_id": targetUserID, "is_admin": req.IsAdmin, "message": "Admin status updated successfully", }) }