260 lines
6.9 KiB
Go
260 lines
6.9 KiB
Go
package handlers
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"html/template"
|
|
"log/slog"
|
|
"net"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
qfassets "git.mchus.pro/mchus/quoteforge"
|
|
"git.mchus.pro/mchus/quoteforge/internal/db"
|
|
"git.mchus.pro/mchus/quoteforge/internal/localdb"
|
|
"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 {
|
|
localDB *localdb.LocalDB
|
|
connMgr *db.ConnectionManager
|
|
templates map[string]*template.Template
|
|
restartSig chan struct{}
|
|
}
|
|
|
|
var errPermissionProbeRollback = errors.New("permission probe rollback")
|
|
|
|
func NewSetupHandler(localDB *localdb.LocalDB, connMgr *db.ConnectionManager, _ string, restartSig chan struct{}) (*SetupHandler, error) {
|
|
funcMap := template.FuncMap{
|
|
"sub": func(a, b int) int { return a - b },
|
|
"add": func(a, b int) int { return a + b },
|
|
}
|
|
|
|
templates := make(map[string]*template.Template)
|
|
|
|
// Load setup template (standalone, no base needed)
|
|
var tmpl *template.Template
|
|
var err error
|
|
tmpl, err = template.New("").Funcs(funcMap).ParseFS(qfassets.TemplatesFS, "web/templates/setup.html")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing setup template: %w", err)
|
|
}
|
|
templates["setup.html"] = tmpl
|
|
|
|
return &SetupHandler{
|
|
localDB: localDB,
|
|
connMgr: connMgr,
|
|
templates: templates,
|
|
restartSig: restartSig,
|
|
}, nil
|
|
}
|
|
|
|
// ShowSetup renders the database setup form
|
|
func (h *SetupHandler) ShowSetup(c *gin.Context) {
|
|
c.Header("Content-Type", "text/html; charset=utf-8")
|
|
|
|
// Get existing settings if any
|
|
settings, _ := h.localDB.GetSettings()
|
|
|
|
data := gin.H{
|
|
"Settings": settings,
|
|
}
|
|
|
|
tmpl := h.templates["setup.html"]
|
|
if err := tmpl.ExecuteTemplate(c.Writer, "setup.html", data); err != nil {
|
|
_ = c.Error(err)
|
|
c.String(http.StatusInternalServerError, "Template error")
|
|
}
|
|
}
|
|
|
|
// TestConnection tests the database connection without saving
|
|
func (h *SetupHandler) TestConnection(c *gin.Context) {
|
|
host := c.PostForm("host")
|
|
portStr := c.PostForm("port")
|
|
database := c.PostForm("database")
|
|
user := c.PostForm("user")
|
|
password := c.PostForm("password")
|
|
|
|
port := 3306
|
|
if p, err := strconv.Atoi(portStr); err == nil {
|
|
port = p
|
|
}
|
|
|
|
// If password is empty, try to use saved password
|
|
if password == "" {
|
|
if settings, err := h.localDB.GetSettings(); err == nil && settings != nil {
|
|
password = settings.PasswordEncrypted // GetSettings returns decrypted password in this field
|
|
}
|
|
}
|
|
|
|
dsn := buildMySQLDSN(host, port, database, user, password, 5*time.Second)
|
|
lotCount, canWrite, err := validateMariaDBConnection(dsn)
|
|
if err != nil {
|
|
_ = c.Error(err)
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"error": "Connection check failed",
|
|
})
|
|
return
|
|
}
|
|
|
|
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) {
|
|
existingSettings, _ := h.localDB.GetSettings()
|
|
|
|
host := c.PostForm("host")
|
|
portStr := c.PostForm("port")
|
|
database := c.PostForm("database")
|
|
user := c.PostForm("user")
|
|
password := c.PostForm("password")
|
|
|
|
port := 3306
|
|
if p, err := strconv.Atoi(portStr); err == nil {
|
|
port = p
|
|
}
|
|
|
|
// If password is empty, use saved password
|
|
if password == "" {
|
|
if settings, err := h.localDB.GetSettings(); err == nil && settings != nil {
|
|
password = settings.PasswordEncrypted // GetSettings returns decrypted password in this field
|
|
}
|
|
}
|
|
|
|
// Test connection first
|
|
dsn := buildMySQLDSN(host, port, database, user, password, 5*time.Second)
|
|
if _, _, err := validateMariaDBConnection(dsn); err != nil {
|
|
_ = c.Error(err)
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"success": false,
|
|
"error": "Connection check failed",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Save settings
|
|
if err := h.localDB.SaveSettings(host, port, database, user, password); err != nil {
|
|
_ = c.Error(err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"success": false,
|
|
"error": "Failed to save settings",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Try to connect immediately to verify settings
|
|
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")
|
|
}
|
|
}
|
|
|
|
settingsChanged := existingSettings == nil ||
|
|
existingSettings.Host != host ||
|
|
existingSettings.Port != port ||
|
|
existingSettings.Database != database ||
|
|
existingSettings.User != user ||
|
|
existingSettings.PasswordEncrypted != password
|
|
|
|
restartQueued := settingsChanged && h.restartSig != nil
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "Settings saved.",
|
|
"restart_required": settingsChanged,
|
|
"restart_queued": restartQueued,
|
|
})
|
|
|
|
// Signal restart after response is sent (if restart signal is configured)
|
|
if restartQueued {
|
|
go func() {
|
|
time.Sleep(500 * time.Millisecond) // Give time for response to be sent
|
|
select {
|
|
case h.restartSig <- struct{}{}:
|
|
default:
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
// GetStatus returns the current setup status
|
|
func (h *SetupHandler) GetStatus(c *gin.Context) {
|
|
hasSettings := h.localDB.HasSettings()
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"configured": hasSettings,
|
|
})
|
|
}
|
|
|
|
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()
|
|
}
|
|
|
|
func validateMariaDBConnection(dsn string) (int64, bool, error) {
|
|
db, err := gorm.Open(gormmysql.Open(dsn), &gorm.Config{
|
|
Logger: logger.Default.LogMode(logger.Silent),
|
|
})
|
|
if err != nil {
|
|
return 0, false, fmt.Errorf("open MariaDB connection: %w", err)
|
|
}
|
|
|
|
sqlDB, err := db.DB()
|
|
if err != nil {
|
|
return 0, false, fmt.Errorf("get database handle: %w", err)
|
|
}
|
|
defer sqlDB.Close()
|
|
|
|
if err := sqlDB.Ping(); err != nil {
|
|
return 0, false, fmt.Errorf("ping MariaDB: %w", err)
|
|
}
|
|
|
|
var lotCount int64
|
|
if err := db.Table("lot").Count(&lotCount).Error; err != nil {
|
|
return 0, false, fmt.Errorf("check required table lot: %w", err)
|
|
}
|
|
|
|
return lotCount, testSyncWritePermission(db), nil
|
|
}
|
|
|
|
func testSyncWritePermission(db *gorm.DB) bool {
|
|
sentinel := fmt.Sprintf("quoteforge-permission-check-%d", time.Now().UnixNano())
|
|
err := db.Transaction(func(tx *gorm.DB) error {
|
|
if err := tx.Exec(`
|
|
INSERT INTO qt_client_schema_state (username, hostname, last_checked_at, updated_at)
|
|
VALUES (?, ?, NOW(), NOW())
|
|
ON DUPLICATE KEY UPDATE
|
|
last_checked_at = VALUES(last_checked_at),
|
|
updated_at = VALUES(updated_at)
|
|
`, sentinel, "setup-check").Error; err != nil {
|
|
return err
|
|
}
|
|
return errPermissionProbeRollback
|
|
})
|
|
|
|
return errors.Is(err, errPermissionProbeRollback)
|
|
}
|