443 lines
13 KiB
Go
443 lines
13 KiB
Go
package api
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"jiggablend/pkg/types"
|
|
)
|
|
|
|
// handleGenerateRunnerAPIKey generates a new runner API key
|
|
func (s *Manager) 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 *Manager) 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 *Manager) 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 *Manager) 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 *Manager) 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 *Manager) 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 *Manager) 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()
|
|
|
|
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
|
|
}
|
|
|
|
// In polling model, database status is the source of truth
|
|
// Runners update their status when they poll for jobs
|
|
runners = append(runners, map[string]interface{}{
|
|
"id": runner.ID,
|
|
"name": runner.Name,
|
|
"hostname": runner.Hostname,
|
|
"status": runner.Status,
|
|
"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 *Manager) 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 *Manager) 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,
|
|
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
|
|
err := rows.Scan(
|
|
&job.ID, &job.UserID, &jobType, &job.Name, &job.Status, &job.Progress,
|
|
&frameStart, &frameEnd, &outputFormat,
|
|
&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 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 *Manager) 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 *Manager) 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 *Manager) 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",
|
|
})
|
|
}
|