- new resp package (OK/Page/Fail/BadRequest/.../InternalErr): unified error
envelope {detail, code?}, sanitized 500s (full error logged server-side,
generic message to client - stops DB error leakage), pagination envelope
{items,total,page,page_size}; migrate 200+ inline c.JSON error sites
- refresh token: 7d rotation tokens (SHA-256 hashed at rest), public
POST /auth/refresh with single-use rotation, POST /auth/logout revokes
all user refresh tokens (endpoint previously missing - frontend 404)
- security: gh-ost command line password masked in task status payload;
Instance.MarshalJSON guarantees encrypted passwords never leave the API
(removes reliance on per-handler manual masking); production (DEBUG=false)
with default JWT secret now auto-generates and persists a random secret
- safe.Go recover wrapper for background goroutines (instance probe,
gh-ost async)
- delete dead service layer (zero callers)
- fix pre-existing go vet unkeyed bson.E literals
99 lines
2.5 KiB
Go
99 lines
2.5 KiB
Go
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
"sql-platform/internal/handler/resp"
|
|
"sql-platform/internal/middleware"
|
|
"sql-platform/internal/model"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// GET /api/v1/tickets/scheduled/list
|
|
// Lists tickets that have scheduled_at set and status is "approved" (waiting for scheduled execution).
|
|
func ListScheduledTasks(c *gin.Context) {
|
|
userID, _, role := middleware.GetCurrentUser(c)
|
|
|
|
query := model.DB.Model(&model.Ticket{}).
|
|
Where("scheduled_at IS NOT NULL AND scheduled_at != '' AND status IN ?", []string{"approved", "scheduled"}).
|
|
Order("scheduled_at ASC")
|
|
|
|
// Filter by user role (same logic as ListTickets)
|
|
switch role {
|
|
case "developer":
|
|
query = query.Where("applicant_id = ?", userID)
|
|
case "dba", "auditor":
|
|
query = query.Where(
|
|
"applicant_id = ? OR id IN (SELECT ticket_id FROM ticket_approvals WHERE approver_id = ?)",
|
|
userID, userID,
|
|
)
|
|
// admin sees everything
|
|
}
|
|
|
|
var total int64
|
|
query.Count(&total)
|
|
|
|
var tickets []model.Ticket
|
|
query.Find(&tickets)
|
|
|
|
// Enrich with instance_name and instance_db_type
|
|
type ScheduledTicketItem struct {
|
|
model.Ticket
|
|
InstanceName string `json:"instance_name"`
|
|
InstanceDBType string `json:"instance_db_type"`
|
|
}
|
|
|
|
items := make([]ScheduledTicketItem, 0, len(tickets))
|
|
for _, t := range tickets {
|
|
item := ScheduledTicketItem{
|
|
Ticket: t,
|
|
}
|
|
var instance model.Instance
|
|
if err := model.DB.Select("name, db_type").First(&instance, t.InstanceID).Error; err == nil {
|
|
item.InstanceName = instance.Name
|
|
item.InstanceDBType = instance.DBType
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"items": items,
|
|
"total": total,
|
|
})
|
|
}
|
|
|
|
// POST /api/v1/tickets/:id/cancel-schedule
|
|
// Cancels a scheduled task by clearing scheduled_at and setting status to "cancelled".
|
|
func CancelScheduledTask(c *gin.Context) {
|
|
id := c.Param("id")
|
|
userID, _, role := middleware.GetCurrentUser(c)
|
|
|
|
var ticket model.Ticket
|
|
if err := model.DB.First(&ticket, id).Error; err != nil {
|
|
resp.NotFound(c, "Ticket not found")
|
|
return
|
|
}
|
|
|
|
// Only the applicant or admin/dba can cancel
|
|
if ticket.ApplicantID != userID && role != "admin" && role != "dba" {
|
|
resp.Forbidden(c, "Permission denied")
|
|
return
|
|
}
|
|
|
|
// Only allow cancellation of scheduled tickets
|
|
if !ticket.ScheduledAt.Valid {
|
|
resp.BadRequest(c, "Ticket is not scheduled")
|
|
return
|
|
}
|
|
|
|
status := "cancelled"
|
|
ticket.Status = &status
|
|
ticket.ScheduledAt.Valid = false
|
|
ticket.ScheduledAt.Time = time.Time{}
|
|
model.DB.Save(&ticket)
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "Scheduled task cancelled", "ticket_id": ticket.ID})
|
|
}
|