package handlers import ( "Yimaru-Backend/internal/domain" "crypto/subtle" "fmt" "os" "path/filepath" "sort" "strings" "github.com/gofiber/fiber/v2" ) type resetAndReseedReq struct { Confirm string `json:"confirm"` } // ResetAndReseedDatabase godoc // @Summary Reset and reseed database // @Description Dangerous operation: truncates all public tables (except schema_migrations) and reseeds from db/data SQL files. // @Tags internal // @Accept json // @Produce json // @Param X-Seed-Reset-Token header string true "Reset token configured in DB_RESET_RESEED_TOKEN" // @Param body body resetAndReseedReq true "Confirmation payload" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 403 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/internal/db/reset-reseed [post] func (h *Handler) ResetAndReseedDatabase(c *fiber.Ctx) error { if h.Cfg == nil || !h.Cfg.DBResetReseedEnabled { return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{ Message: "Operation is disabled", Error: "DB_RESET_RESEED_ENABLED must be set to true", }) } var req resetAndReseedReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid request body", Error: err.Error(), }) } if strings.TrimSpace(req.Confirm) != "RESET_AND_RESEED" { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Confirmation required", Error: `set confirm to "RESET_AND_RESEED"`, }) } expectedToken := strings.TrimSpace(h.Cfg.DBResetReseedToken) if expectedToken != "" { providedToken := strings.TrimSpace(c.Get("X-Seed-Reset-Token")) if subtle.ConstantTimeCompare([]byte(providedToken), []byte(expectedToken)) != 1 { return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{ Message: "Invalid reset token", Error: "missing or invalid X-Seed-Reset-Token", }) } } seedDir := strings.TrimSpace(h.Cfg.DBSeedDir) if seedDir == "" { seedDir = "db/data" } seedFiles, err := filepath.Glob(filepath.Join(seedDir, "*.sql")) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to resolve seed files", Error: err.Error(), }) } if len(seedFiles) == 0 { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "No seed files found", Error: fmt.Sprintf("no .sql files found in %s", seedDir), }) } sort.Strings(seedFiles) var sqlBuilder strings.Builder sqlBuilder.WriteString("BEGIN;\n") sqlBuilder.WriteString(` DO $$ DECLARE truncate_stmt text; BEGIN SELECT 'TRUNCATE TABLE ' || string_agg(format('%I.%I', schemaname, tablename), ', ') || ' RESTART IDENTITY CASCADE' INTO truncate_stmt FROM pg_tables WHERE schemaname = 'public' AND tablename <> 'schema_migrations'; IF truncate_stmt IS NOT NULL THEN EXECUTE truncate_stmt; END IF; END $$; `) for _, file := range seedFiles { content, readErr := os.ReadFile(file) if readErr != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to read seed file", Error: fmt.Sprintf("%s: %v", file, readErr), }) } sqlBuilder.WriteString("\n-- seed file: ") sqlBuilder.WriteString(file) sqlBuilder.WriteString("\n") sqlBuilder.Write(content) sqlBuilder.WriteString("\n") } sqlBuilder.WriteString("COMMIT;") if _, err := h.analyticsDB.ExecRaw(c.Context(), sqlBuilder.String()); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to reset and reseed database", Error: err.Error(), }) } // Ensure permission definitions and default assignments are in sync after reseed. if err := h.rbacSvc.SeedPermissions(c.Context()); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Database reseeded but RBAC permission sync failed", Error: err.Error(), }) } if err := h.rbacSvc.SeedDefaultRolePermissions(c.Context()); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Database reseeded but RBAC role default sync failed", Error: err.Error(), }) } if err := h.rbacSvc.Reload(c.Context()); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Database reseeded but RBAC cache reload failed", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Database reset and reseed completed successfully", Data: map[string]interface{}{ "seed_dir": seedDir, "seed_files": seedFiles, "seeded_count": len(seedFiles), }, Success: true, StatusCode: fiber.StatusOK, }) }