101 lines
3.1 KiB
Go
101 lines
3.1 KiB
Go
package admin
|
|
|
|
import (
|
|
"context"
|
|
"crypto/subtle"
|
|
"log"
|
|
"strings"
|
|
"time"
|
|
|
|
"boostai-backend/internal/config"
|
|
"boostai-backend/internal/database"
|
|
"boostai-backend/internal/http/respond"
|
|
"boostai-backend/internal/seeddata"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
)
|
|
|
|
const (
|
|
ReseedHeaderName = "X-Admin-Reseed-Secret"
|
|
ReseedConfirm = "RESEED"
|
|
)
|
|
|
|
type Runner interface {
|
|
Run(ctx context.Context, mockDataDir string) (seeddata.Summary, error)
|
|
}
|
|
|
|
type Handler struct {
|
|
cfg *config.Config
|
|
runner runFunc
|
|
}
|
|
|
|
type runFunc func(timeoutCtx context.Context, mockDataDir string) (seeddata.Summary, error)
|
|
|
|
type reseedRequest struct {
|
|
Confirm string `json:"confirm"`
|
|
}
|
|
|
|
type reseedResponse struct {
|
|
OK bool `json:"ok"`
|
|
Environment string `json:"environment"`
|
|
TriggeredBy string `json:"triggered_by,omitempty"`
|
|
TriggeredAt time.Time `json:"triggered_at"`
|
|
Summary seeddata.Summary `json:"summary"`
|
|
}
|
|
|
|
func NewHandler(db *database.DB, cfg *config.Config) *Handler {
|
|
return &Handler{
|
|
cfg: cfg,
|
|
runner: func(timeoutCtx context.Context, mockDataDir string) (seeddata.Summary, error) {
|
|
return seeddata.Run(timeoutCtx, db, mockDataDir)
|
|
},
|
|
}
|
|
}
|
|
|
|
func (h *Handler) ReseedDatabase(c *fiber.Ctx) error {
|
|
if !h.cfg.AdminReseedEnabled {
|
|
return respond.Error(c, fiber.StatusNotFound, "not_found", "The requested endpoint does not exist")
|
|
}
|
|
|
|
if strings.TrimSpace(h.cfg.AdminReseedSecret) == "" {
|
|
return respond.Error(c, fiber.StatusServiceUnavailable, "admin_reseed_unavailable", "Admin reseed is not configured")
|
|
}
|
|
|
|
providedSecret := strings.TrimSpace(c.Get(ReseedHeaderName))
|
|
if subtle.ConstantTimeCompare([]byte(providedSecret), []byte(h.cfg.AdminReseedSecret)) != 1 {
|
|
return respond.Error(c, fiber.StatusForbidden, "forbidden", "Valid reseed secret required")
|
|
}
|
|
|
|
var req reseedRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "Invalid request body")
|
|
}
|
|
if strings.TrimSpace(req.Confirm) != ReseedConfirm {
|
|
return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "confirm must equal RESEED")
|
|
}
|
|
|
|
triggeredBy, _ := c.Locals("auth.email").(string)
|
|
userID, _ := c.Locals("auth.user_id").(int64)
|
|
startedAt := time.Now().UTC()
|
|
log.Printf("admin reseed requested environment=%s user_id=%d email=%s ip=%s", h.cfg.Environment, userID, triggeredBy, c.IP())
|
|
|
|
timeoutCtx, cancelTimeout := context.WithTimeout(context.Background(), 2*time.Minute)
|
|
defer cancelTimeout()
|
|
|
|
summary, err := h.runner(timeoutCtx, h.cfg.MockDataDir)
|
|
if err != nil {
|
|
log.Printf("admin reseed failed environment=%s user_id=%d email=%s err=%v", h.cfg.Environment, userID, triggeredBy, err)
|
|
return respond.Error(c, fiber.StatusInternalServerError, "admin_reseed_failed", err.Error())
|
|
}
|
|
|
|
log.Printf("admin reseed completed environment=%s user_id=%d email=%s users=%d assignments=%d student_answers=%d", h.cfg.Environment, userID, triggeredBy, summary.Users, summary.Assignments, summary.StudentAnswers)
|
|
|
|
return c.JSON(reseedResponse{
|
|
OK: true,
|
|
Environment: h.cfg.Environment,
|
|
TriggeredBy: triggeredBy,
|
|
TriggeredAt: startedAt,
|
|
Summary: summary,
|
|
})
|
|
}
|