package api import ( "database/sql" "encoding/json" "fmt" "net/http" "time" "jiggablend/pkg/types" ) // handleGenerateRegistrationToken generates a new registration token func (s *Server) handleGenerateRegistrationToken(w http.ResponseWriter, r *http.Request) { userID, err := getUserID(r) if err != nil { s.respondError(w, http.StatusUnauthorized, err.Error()) return } // Default expiration: 24 hours expiresIn := 24 * time.Hour var req struct { ExpiresInHours int `json:"expires_in_hours,omitempty"` } if r.Body != nil && r.ContentLength > 0 { if err := json.NewDecoder(r.Body).Decode(&req); err == nil { if req.ExpiresInHours == 0 { // 0 hours means infinite expiration expiresIn = 0 } else if req.ExpiresInHours > 0 { expiresIn = time.Duration(req.ExpiresInHours) * time.Hour } } } token, err := s.secrets.GenerateRegistrationToken(userID, expiresIn) if err != nil { s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to generate token: %v", err)) return } response := map[string]interface{}{ "token": token, } if expiresIn == 0 { response["expires_in"] = "infinite" response["expires_at"] = nil } else { response["expires_in"] = expiresIn.String() response["expires_at"] = time.Now().Add(expiresIn) } s.respondJSON(w, http.StatusCreated, response) } // handleListRegistrationTokens lists all registration tokens func (s *Server) handleListRegistrationTokens(w http.ResponseWriter, r *http.Request) { tokens, err := s.secrets.ListRegistrationTokens() if err != nil { s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to list tokens: %v", err)) return } s.respondJSON(w, http.StatusOK, tokens) } // handleRevokeRegistrationToken revokes a registration token func (s *Server) handleRevokeRegistrationToken(w http.ResponseWriter, r *http.Request) { tokenID, err := parseID(r, "id") if err != nil { s.respondError(w, http.StatusBadRequest, err.Error()) return } if err := s.secrets.RevokeRegistrationToken(tokenID); err != nil { s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to revoke token: %v", err)) return } s.respondJSON(w, http.StatusOK, map[string]string{"message": "Token revoked"}) } // 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.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.Exec("UPDATE runners SET verified = 1 WHERE id = ?", runnerID) 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.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.Exec("DELETE FROM runners WHERE id = ?", runnerID) 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) { rows, err := s.db.Query( `SELECT id, name, hostname, ip_address, status, last_heartbeat, capabilities, registration_token, verified, priority, created_at FROM runners ORDER BY created_at DESC`, ) if err != nil { s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to query runners: %v", err)) return } defer rows.Close() runners := []map[string]interface{}{} for rows.Next() { var runner types.Runner var registrationToken sql.NullString var verified bool err := rows.Scan( &runner.ID, &runner.Name, &runner.Hostname, &runner.IPAddress, &runner.Status, &runner.LastHeartbeat, &runner.Capabilities, ®istrationToken, &verified, &runner.Priority, &runner.CreatedAt, ) if err != nil { s.respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to scan runner: %v", err)) return } runners = append(runners, map[string]interface{}{ "id": runner.ID, "name": runner.Name, "hostname": runner.Hostname, "ip_address": runner.IPAddress, "status": runner.Status, "last_heartbeat": runner.LastHeartbeat, "capabilities": runner.Capabilities, "registration_token": registrationToken.String, "verified": verified, "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 } rows, err := s.db.Query( `SELECT id, email, name, oauth_provider, is_admin, created_at FROM users ORDER BY created_at DESC`, ) 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.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.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 } rows, err := s.db.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, ) 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, "Invalid request body") 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, "Invalid request body") 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", }) }