package handlers import ( "fmt" "html/template" "log/slog" "net" "net/http" "os" "path/filepath" "strconv" "strings" "time" qfassets "git.mchus.pro/mchus/priceforge" "git.mchus.pro/mchus/priceforge/internal/config" "git.mchus.pro/mchus/priceforge/internal/db" "github.com/gin-gonic/gin" mysqlDriver "github.com/go-sql-driver/mysql" gormmysql "gorm.io/driver/mysql" "gorm.io/gorm" "gorm.io/gorm/logger" ) type SetupHandler struct { connMgr *db.ConnectionManager templates map[string]*template.Template cfg *config.Config saveConfig func(*config.Config) error } type SetupViewSettings struct { Host string Port int Database string User string } func NewSetupHandler(connMgr *db.ConnectionManager, templatesPath string, cfg *config.Config, saveConfig func(*config.Config) error) (*SetupHandler, error) { funcMap := template.FuncMap{ "eq": func(a, b interface{}) bool { return a == b }, "sub": func(a, b int) int { return a - b }, "add": func(a, b int) int { return a + b }, } templates := make(map[string]*template.Template) basePath := filepath.Join(templatesPath, "base.html") setupPath := filepath.Join(templatesPath, "setup.html") var tmpl *template.Template var err error if stat, statErr := os.Stat(templatesPath); statErr == nil && stat.IsDir() { tmpl, err = template.New("").Funcs(funcMap).ParseFiles(basePath, setupPath) } else { tmpl, err = template.New("").Funcs(funcMap).ParseFS( qfassets.TemplatesFS, "web/templates/base.html", "web/templates/setup.html", ) } if err != nil { return nil, fmt.Errorf("parsing setup template: %w", err) } templates["setup.html"] = tmpl return &SetupHandler{ connMgr: connMgr, templates: templates, cfg: cfg, saveConfig: saveConfig, }, nil } // ShowSetup renders the database setup form func (h *SetupHandler) ShowSetup(c *gin.Context) { c.Header("Content-Type", "text/html; charset=utf-8") data := gin.H{ "ActivePage": "setup", "Settings": SetupViewSettings{ Host: h.cfg.Database.Host, Port: h.cfg.Database.Port, Database: h.cfg.Database.Name, User: h.cfg.Database.User, }, } tmpl := h.templates["setup.html"] if err := tmpl.ExecuteTemplate(c.Writer, "setup.html", data); err != nil { c.String(http.StatusInternalServerError, "Template error: %v", err) } } // TestConnection tests the database connection without saving func (h *SetupHandler) TestConnection(c *gin.Context) { host := strings.TrimSpace(c.PostForm("host")) portStr := strings.TrimSpace(c.PostForm("port")) database := strings.TrimSpace(c.PostForm("database")) user := strings.TrimSpace(c.PostForm("user")) password := c.PostForm("password") port := h.cfg.Database.Port if port == 0 { port = 3306 } if portStr != "" { p, err := strconv.Atoi(portStr) if err != nil || p <= 0 { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "error": "Invalid port.", }) return } port = p } if password == "" { password = h.cfg.Database.Password } if host == "" || database == "" || user == "" { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "error": "Host, database and user are required.", }) return } dsn := buildMySQLDSN(host, port, database, user, password, 5*time.Second) db, err := gorm.Open(gormmysql.Open(dsn), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, "error": fmt.Sprintf("Connection failed: %v", err), }) return } sqlDB, err := db.DB() if err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, "error": fmt.Sprintf("Failed to get database handle: %v", err), }) return } defer sqlDB.Close() if err := sqlDB.Ping(); err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, "error": fmt.Sprintf("Ping failed: %v", err), }) return } // Check for required tables var lotCount int64 if err := db.Table("lot").Count(&lotCount).Error; err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, "error": fmt.Sprintf("Table 'lot' not found or inaccessible: %v", err), }) return } // Check write permission canWrite := testWritePermission(db) c.JSON(http.StatusOK, gin.H{ "success": true, "lot_count": lotCount, "can_write": canWrite, "message": fmt.Sprintf("Connected successfully! Found %d components.", lotCount), }) } // SaveConnection saves the connection settings and signals restart func (h *SetupHandler) SaveConnection(c *gin.Context) { host := strings.TrimSpace(c.PostForm("host")) portStr := strings.TrimSpace(c.PostForm("port")) database := strings.TrimSpace(c.PostForm("database")) user := strings.TrimSpace(c.PostForm("user")) password := c.PostForm("password") port := h.cfg.Database.Port if port == 0 { port = 3306 } if portStr != "" { p, err := strconv.Atoi(portStr) if err != nil || p <= 0 { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "error": "Invalid port.", }) return } port = p } if password == "" { password = h.cfg.Database.Password } if host == "" || database == "" || user == "" { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "error": "Host, database and user are required.", }) return } dsn := buildMySQLDSN(host, port, database, user, password, 5*time.Second) db, err := gorm.Open(gormmysql.Open(dsn), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "error": fmt.Sprintf("Connection failed: %v", err), }) return } sqlDB, err := db.DB() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "error": fmt.Sprintf("Failed to get database handle: %v", err), }) return } defer sqlDB.Close() if err := sqlDB.Ping(); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "error": fmt.Sprintf("Ping failed: %v", err), }) return } settingsChanged := h.cfg.Database.Host != host || h.cfg.Database.Port != port || h.cfg.Database.Name != database || h.cfg.Database.User != user || h.cfg.Database.Password != password h.cfg.Database.Host = host h.cfg.Database.Port = port h.cfg.Database.Name = database h.cfg.Database.User = user h.cfg.Database.Password = password if err := h.saveConfig(h.cfg); err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "error": fmt.Sprintf("Failed to save config: %v", err), }) return } if h.connMgr != nil { if err := h.connMgr.TryConnect(); err != nil { slog.Warn("failed to connect after saving settings", "error", err) } else { slog.Info("successfully connected to database after saving settings") } } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Settings saved.", "restart_required": settingsChanged, }) } func testWritePermission(db *gorm.DB) bool { // Simple check: try to create a temporary table and drop it testTable := fmt.Sprintf("qt_write_test_%d", time.Now().UnixNano()) // Try to create a test table err := db.Exec(fmt.Sprintf("CREATE TABLE %s (id INT)", testTable)).Error if err != nil { return false } // Drop it immediately db.Exec(fmt.Sprintf("DROP TABLE %s", testTable)) return true } func buildMySQLDSN(host string, port int, database, user, password string, timeout time.Duration) string { cfg := mysqlDriver.NewConfig() cfg.User = user cfg.Passwd = password cfg.Net = "tcp" cfg.Addr = net.JoinHostPort(host, strconv.Itoa(port)) cfg.DBName = database cfg.ParseTime = true cfg.Loc = time.Local cfg.Timeout = timeout cfg.Params = map[string]string{ "charset": "utf8mb4", } return cfg.FormatDSN() }