Files
PriceForge/internal/handlers/setup.go
2026-03-07 21:10:20 +03:00

312 lines
7.6 KiB
Go

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()
}