Files
bookhoard/internal/handlers/jobs.go
T
John O'Keefe bb2cc26e9d feat(scanner): scanner entrypoints + generic jobs carry RLS identity; SeedDB harness fallback
The full suite enumerated the remaining triage class: tests drive the
scanner directly (ScanFolders(context.Background())) and read seeded
media via Queries with bare contexts — every governed read raised.

- MediaScanner.ScanFolders self-declares its configured admin
  (SetAdminID — set by main AND the test helpers): a scan is server
  maintenance, product-correct regardless of caller.
- ScopedDB gains a Fallback identity, exposed as NewSeedDB for the
  TEST HARNESS ONLY: setup.DB falls back to rls.SeedIdentity when the
  context carries none, so direct seeding/verification helpers work
  uniformly instead of per-site context threading. rls.SeedIdentity is
  the shared constant (no library_visibility rows => full visibility).
- generic job creation (jobs.go) stamps the requesting admin into the
  job Context, same as the scan endpoint.

The sync queue processor stays identity-free: sync_queue is not an
RLS-governed table.
2026-10-29 09:27:04 -04:00

96 lines
2.2 KiB
Go

package handlers
import (
"context"
"net/http"
"github.com/google/uuid"
"github.com/labstack/echo/v5"
"github.com/jackc/pgx/v5/pgtype"
"bookhoard/internal/database"
"bookhoard/internal/rls"
"bookhoard/internal/services"
)
type JobsHandler struct {
db *database.Queries
worker *services.Worker
}
func NewJobsHandler(db *database.Queries, worker *services.Worker) *JobsHandler {
return &JobsHandler{
db: db,
worker: worker,
}
}
// CreateJob creates a new job based on type
func (h *JobsHandler) CreateJob(c *echo.Context) error {
var req struct {
Type string `json:"type"`
Params map[string]interface{} `json:"params"`
}
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid request body",
})
}
// Validate job type
var jobType services.JobType
switch req.Type {
case "import", "convert", "thumbnails", "backup", "analytics":
jobType = services.JobType(req.Type)
default:
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid job type",
})
}
// Add database to params
req.Params["db"] = h.db
// RLS plan §6: the job's Context carries the requesting admin's
// identity — governed reads inside the job would raise otherwise.
userUUID, _ := uuid.Parse(c.Get("user_id").(string))
// Create job
job := &services.Job{
ID: uuid.New().String(),
Type: jobType,
Params: req.Params,
Status: services.JobStatusPending,
Context: rls.WithUser(context.Background(), pgtype.UUID{Bytes: userUUID, Valid: true}),
}
// Enqueue job
if err := h.worker.EnqueueJob(job); err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to enqueue job",
})
}
return c.JSON(http.StatusAccepted, map[string]interface{}{
"message": "Job created",
"job_id": job.ID,
"type": req.Type,
"status": "pending",
})
}
// GetJobStatus returns the status of a specific job
func (h *JobsHandler) GetJobStatus(c *echo.Context) error {
jobID := c.Param("jobId")
result, exists := h.worker.GetJobStatus(jobID)
if !exists {
return c.JSON(http.StatusNotFound, map[string]string{
"error": "Job not found",
})
}
return c.JSON(http.StatusOK, result)
}