package reseed import ( "context" "crypto/sha256" "crypto/subtle" "encoding/hex" "fmt" "html" "log" "strings" "time" "boostai-backend/internal/config" "boostai-backend/internal/database" "boostai-backend/internal/seeddata" "github.com/gofiber/fiber/v2" ) const reseedCookieName = "boostai_reseed_auth" type runFunc func(ctx context.Context, mockDataDir string) (seeddata.Summary, error) type Handler struct { cfg *config.Config runner runFunc } type pageData struct { Authorized bool Environment string MockDataDir string Error string Success string Summary *seeddata.Summary } func NewHandler(db *database.DB, cfg *config.Config) *Handler { return &Handler{ cfg: cfg, runner: func(ctx context.Context, mockDataDir string) (seeddata.Summary, error) { return seeddata.Run(ctx, db, mockDataDir) }, } } func (h *Handler) Page(c *fiber.Ctx) error { if !h.cfg.AdminReseedEnabled { return fiber.ErrNotFound } return h.renderPage(c, pageData{ Authorized: h.isAuthorized(c), Environment: h.cfg.Environment, MockDataDir: h.cfg.MockDataDir, }) } func (h *Handler) Login(c *fiber.Ctx) error { if !h.cfg.AdminReseedEnabled { return fiber.ErrNotFound } password := strings.TrimSpace(c.FormValue("password")) if subtle.ConstantTimeCompare([]byte(password), []byte(h.cfg.ReseedPagePassword)) != 1 { return h.renderPage(c.Status(fiber.StatusUnauthorized), pageData{ Authorized: false, Environment: h.cfg.Environment, MockDataDir: h.cfg.MockDataDir, Error: "Invalid password", }) } h.setAuthCookie(c) return c.Redirect("/reseed", fiber.StatusSeeOther) } func (h *Handler) Run(c *fiber.Ctx) error { if !h.cfg.AdminReseedEnabled { return fiber.ErrNotFound } if !h.isAuthorized(c) { return h.renderPage(c.Status(fiber.StatusUnauthorized), pageData{ Authorized: false, Environment: h.cfg.Environment, MockDataDir: h.cfg.MockDataDir, Error: "Please unlock the reseed page first", }) } startedAt := time.Now().UTC() log.Printf("browser reseed requested environment=%s ip=%s", h.cfg.Environment, c.IP()) timeoutCtx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() summary, err := h.runner(timeoutCtx, h.cfg.MockDataDir) if err != nil { log.Printf("browser reseed failed environment=%s ip=%s err=%v", h.cfg.Environment, c.IP(), err) return h.renderPage(c.Status(fiber.StatusInternalServerError), pageData{ Authorized: true, Environment: h.cfg.Environment, MockDataDir: h.cfg.MockDataDir, Error: err.Error(), }) } log.Printf("browser reseed completed environment=%s ip=%s users=%d assignments=%d student_answers=%d", h.cfg.Environment, c.IP(), summary.Users, summary.Assignments, summary.StudentAnswers) return h.renderPage(c, pageData{ Authorized: true, Environment: h.cfg.Environment, MockDataDir: h.cfg.MockDataDir, Success: fmt.Sprintf("Reseed completed at %s UTC", startedAt.Format("2006-01-02 15:04:05")), Summary: &summary, }) } func (h *Handler) Logout(c *fiber.Ctx) error { if !h.cfg.AdminReseedEnabled { return fiber.ErrNotFound } h.clearAuthCookie(c) return c.Redirect("/reseed", fiber.StatusSeeOther) } func (h *Handler) isAuthorized(c *fiber.Ctx) bool { provided := strings.TrimSpace(c.Cookies(reseedCookieName)) expected := h.authCookieValue() if provided == "" || expected == "" { return false } return subtle.ConstantTimeCompare([]byte(provided), []byte(expected)) == 1 } func (h *Handler) setAuthCookie(c *fiber.Ctx) { c.Cookie(&fiber.Cookie{ Name: reseedCookieName, Value: h.authCookieValue(), HTTPOnly: true, Secure: h.cfg.IsProduction(), SameSite: fiber.CookieSameSiteLaxMode, Path: "/", Expires: time.Now().UTC().Add(12 * time.Hour), }) } func (h *Handler) clearAuthCookie(c *fiber.Ctx) { c.Cookie(&fiber.Cookie{ Name: reseedCookieName, Value: "", HTTPOnly: true, Secure: h.cfg.IsProduction(), SameSite: fiber.CookieSameSiteLaxMode, Path: "/", Expires: time.Unix(0, 0), MaxAge: -1, }) } func (h *Handler) authCookieValue() string { sum := sha256.Sum256([]byte(h.cfg.JWTSecret + "|" + h.cfg.ReseedPagePassword)) return hex.EncodeToString(sum[:]) } func (h *Handler) renderPage(c *fiber.Ctx, data pageData) error { content := reseedPageHTML(data) c.Type("html", "utf-8") return c.SendString(content) } func reseedPageHTML(data pageData) string { var statusHTML strings.Builder if data.Error != "" { statusHTML.WriteString(`
`) statusHTML.WriteString(html.EscapeString(fmt.Sprintf( "users: %d\nclassrooms: %d\nquestions: %d\ntags: %d\nassignments: %d\nassignment_links: %d\nstudent_answers: %d\nmock_data_dir: %s", data.Summary.Users, data.Summary.Classrooms, data.Summary.Questions, data.Summary.Tags, data.Summary.Assignments, data.Summary.AssignmentLinks, data.Summary.StudentAnswers, data.Summary.MockDataDir, ))) statusHTML.WriteString(``) } var body strings.Builder if data.Authorized { body.WriteString(`
This will clear seeded app data and repopulate it from Mock-Data.