All checks were successful
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
310 lines
9.0 KiB
Go
310 lines
9.0 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/robfig/cron/v3"
|
|
|
|
"switchboard-core/models"
|
|
"switchboard-core/store"
|
|
"switchboard-core/triggers"
|
|
)
|
|
|
|
// ScheduleHandler provides CRUD for user-created scheduled tasks.
|
|
type ScheduleHandler struct {
|
|
stores store.Stores
|
|
engine *triggers.Engine
|
|
}
|
|
|
|
// NewScheduleHandler creates a ScheduleHandler.
|
|
func NewScheduleHandler(stores store.Stores, engine *triggers.Engine) *ScheduleHandler {
|
|
return &ScheduleHandler{stores: stores, engine: engine}
|
|
}
|
|
|
|
// ── User Endpoints ───────────────────────────
|
|
|
|
// ListMySchedules returns schedules created by the current user.
|
|
// GET /api/v1/schedules
|
|
func (h *ScheduleHandler) ListMySchedules(c *gin.Context) {
|
|
userID := c.GetString("user_id")
|
|
tasks, err := h.stores.ScheduledTasks.ListByCreator(c.Request.Context(), userID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"schedules": tasks})
|
|
}
|
|
|
|
// CreateSchedule creates a new scheduled task.
|
|
// POST /api/v1/schedules
|
|
func (h *ScheduleHandler) CreateSchedule(c *gin.Context) {
|
|
var req struct {
|
|
Name string `json:"name" binding:"required"`
|
|
Description string `json:"description"`
|
|
CronExpr string `json:"cron_expr" binding:"required"`
|
|
Script string `json:"script"`
|
|
TemplateID string `json:"template_id"`
|
|
TemplateParams any `json:"template_params"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Validate cron expression
|
|
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
|
|
schedule, err := parser.Parse(req.CronExpr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid cron expression: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
nextFire := schedule.Next(time.Now())
|
|
userID := c.GetString("user_id")
|
|
|
|
task := &models.ScheduledTask{
|
|
Name: req.Name,
|
|
Description: req.Description,
|
|
CreatorID: userID,
|
|
RunAs: models.RunAsCreator,
|
|
CronExpr: req.CronExpr,
|
|
NextFireAt: &nextFire,
|
|
Enabled: true,
|
|
Script: req.Script,
|
|
TemplateID: req.TemplateID,
|
|
}
|
|
|
|
if err := h.stores.ScheduledTasks.Create(c.Request.Context(), task); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Register with engine
|
|
h.engine.RegisterSchedule(task)
|
|
|
|
c.JSON(http.StatusCreated, task)
|
|
}
|
|
|
|
// GetSchedule returns a single scheduled task.
|
|
// GET /api/v1/schedules/:id
|
|
func (h *ScheduleHandler) GetSchedule(c *gin.Context) {
|
|
task, err := h.stores.ScheduledTasks.GetByID(c.Request.Context(), c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if task == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "schedule not found"})
|
|
return
|
|
}
|
|
|
|
// Check ownership (non-admin can only see own schedules)
|
|
userID := c.GetString("user_id")
|
|
if task.CreatorID != userID {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "schedule not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, task)
|
|
}
|
|
|
|
// UpdateSchedule updates a scheduled task.
|
|
// PUT /api/v1/schedules/:id
|
|
func (h *ScheduleHandler) UpdateSchedule(c *gin.Context) {
|
|
task, err := h.stores.ScheduledTasks.GetByID(c.Request.Context(), c.Param("id"))
|
|
if err != nil || task == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "schedule not found"})
|
|
return
|
|
}
|
|
|
|
userID := c.GetString("user_id")
|
|
if task.CreatorID != userID {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "schedule not found"})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Name *string `json:"name"`
|
|
Description *string `json:"description"`
|
|
CronExpr *string `json:"cron_expr"`
|
|
Script *string `json:"script"`
|
|
Enabled *bool `json:"enabled"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if req.Name != nil {
|
|
task.Name = *req.Name
|
|
}
|
|
if req.Description != nil {
|
|
task.Description = *req.Description
|
|
}
|
|
if req.Script != nil {
|
|
task.Script = *req.Script
|
|
}
|
|
if req.Enabled != nil {
|
|
task.Enabled = *req.Enabled
|
|
}
|
|
if req.CronExpr != nil {
|
|
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
|
|
schedule, err := parser.Parse(*req.CronExpr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid cron expression: " + err.Error()})
|
|
return
|
|
}
|
|
task.CronExpr = *req.CronExpr
|
|
nextFire := schedule.Next(time.Now())
|
|
task.NextFireAt = &nextFire
|
|
}
|
|
|
|
if err := h.stores.ScheduledTasks.Update(c.Request.Context(), task); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Re-register with engine
|
|
h.engine.UnregisterSchedule(task.ID)
|
|
if task.Enabled {
|
|
h.engine.RegisterSchedule(task)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, task)
|
|
}
|
|
|
|
// DeleteSchedule deletes a scheduled task.
|
|
// DELETE /api/v1/schedules/:id
|
|
func (h *ScheduleHandler) DeleteSchedule(c *gin.Context) {
|
|
task, err := h.stores.ScheduledTasks.GetByID(c.Request.Context(), c.Param("id"))
|
|
if err != nil || task == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "schedule not found"})
|
|
return
|
|
}
|
|
|
|
userID := c.GetString("user_id")
|
|
if task.CreatorID != userID {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "schedule not found"})
|
|
return
|
|
}
|
|
|
|
h.engine.UnregisterSchedule(task.ID)
|
|
if err := h.stores.ScheduledTasks.Delete(c.Request.Context(), task.ID); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
}
|
|
|
|
// RunSchedule manually triggers a scheduled task (test run).
|
|
// POST /api/v1/schedules/:id/run
|
|
func (h *ScheduleHandler) RunSchedule(c *gin.Context) {
|
|
task, err := h.stores.ScheduledTasks.GetByID(c.Request.Context(), c.Param("id"))
|
|
if err != nil || task == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "schedule not found"})
|
|
return
|
|
}
|
|
|
|
userID := c.GetString("user_id")
|
|
if task.CreatorID != userID {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "schedule not found"})
|
|
return
|
|
}
|
|
|
|
// Fire immediately in a goroutine
|
|
go h.engine.ManualRun(task)
|
|
|
|
c.JSON(http.StatusAccepted, gin.H{"ok": true, "message": "task queued for execution"})
|
|
}
|
|
|
|
// ListScheduleLogs returns execution logs for a scheduled task.
|
|
// GET /api/v1/schedules/:id/logs
|
|
func (h *ScheduleHandler) ListScheduleLogs(c *gin.Context) {
|
|
task, err := h.stores.ScheduledTasks.GetByID(c.Request.Context(), c.Param("id"))
|
|
if err != nil || task == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "schedule not found"})
|
|
return
|
|
}
|
|
|
|
userID := c.GetString("user_id")
|
|
if task.CreatorID != userID {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "schedule not found"})
|
|
return
|
|
}
|
|
|
|
limit := parseIntParam(c, "limit", 50)
|
|
logs, err := h.stores.Triggers.ListLogs(c.Request.Context(), task.ID, limit)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"logs": logs})
|
|
}
|
|
|
|
// ── Admin Endpoints ──────────────────────────
|
|
|
|
// AdminListSchedules returns all scheduled tasks (admin view).
|
|
// GET /admin/schedules
|
|
func (h *ScheduleHandler) AdminListSchedules(c *gin.Context) {
|
|
opts := store.ScheduledTaskListOptions{
|
|
ListOptions: store.ListOptions{
|
|
Limit: parseIntParam(c, "limit", 50),
|
|
Offset: parseIntParam(c, "offset", 0),
|
|
},
|
|
CreatorID: c.Query("creator_id"),
|
|
}
|
|
if e := c.Query("enabled"); e != "" {
|
|
b := e == "true"
|
|
opts.Enabled = &b
|
|
}
|
|
|
|
tasks, total, err := h.stores.ScheduledTasks.List(c.Request.Context(), opts)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"schedules": tasks, "total": total})
|
|
}
|
|
|
|
// AdminEnableSchedule enables a scheduled task.
|
|
// PUT /admin/schedules/:id/enable
|
|
func (h *ScheduleHandler) AdminEnableSchedule(c *gin.Context) {
|
|
id := c.Param("id")
|
|
if err := h.stores.ScheduledTasks.SetEnabled(c.Request.Context(), id, true); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
task, _ := h.stores.ScheduledTasks.GetByID(c.Request.Context(), id)
|
|
if task != nil {
|
|
h.engine.RegisterSchedule(task)
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
}
|
|
|
|
// AdminDisableSchedule disables a scheduled task.
|
|
// PUT /admin/schedules/:id/disable
|
|
func (h *ScheduleHandler) AdminDisableSchedule(c *gin.Context) {
|
|
id := c.Param("id")
|
|
h.engine.UnregisterSchedule(id)
|
|
if err := h.stores.ScheduledTasks.SetEnabled(c.Request.Context(), id, false); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
}
|
|
|
|
// AdminDeleteSchedule deletes a scheduled task.
|
|
// DELETE /admin/schedules/:id
|
|
func (h *ScheduleHandler) AdminDeleteSchedule(c *gin.Context) {
|
|
id := c.Param("id")
|
|
h.engine.UnregisterSchedule(id)
|
|
if err := h.stores.ScheduledTasks.Delete(c.Request.Context(), id); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
}
|