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.
96 lines
2.2 KiB
Go
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)
|
|
}
|