Restore connection settings modal from top nav

This commit is contained in:
2026-02-08 10:54:31 +03:00
parent eb09177f21
commit 3f7920d877
2 changed files with 313 additions and 6 deletions

View File

@@ -12,6 +12,8 @@ import (
"os/signal"
"path/filepath"
"runtime"
"strconv"
"strings"
"syscall"
"time"
@@ -29,6 +31,8 @@ import (
"git.mchus.pro/mchus/priceforge/internal/services/pricelist"
"git.mchus.pro/mchus/priceforge/internal/services/pricing"
"github.com/gin-gonic/gin"
mysqlDriver "github.com/go-sql-driver/mysql"
"gopkg.in/yaml.v3"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
@@ -78,6 +82,16 @@ func main() {
os.Exit(1)
}
setConfigDefaults(cfg)
cfg.Server.Host = normalizeLocalHost(cfg.Server.Host)
if !isLoopbackHost(cfg.Server.Host) {
slog.Error(
"server host must be loopback-only for local mode",
"host", cfg.Server.Host,
"allowed", "127.0.0.1/localhost/::1",
"config_path", resolvedConfigPath,
)
os.Exit(1)
}
slog.Info("resolved runtime files", "config_path", resolvedConfigPath)
setupLogger(cfg.Logging)
@@ -144,7 +158,7 @@ func main() {
}
gin.SetMode(cfg.Server.Mode)
router, err := setupRouter(cfg, connMgr, mariaDB, dbUser)
router, err := setupRouter(cfg, resolvedConfigPath, connMgr, mariaDB, dbUser)
if err != nil {
slog.Error("failed to setup router", "error", err)
os.Exit(1)
@@ -231,6 +245,29 @@ func setConfigDefaults(cfg *config.Config) {
}
}
func isLoopbackHost(host string) bool {
h := strings.TrimSpace(strings.ToLower(host))
if h == "" {
return false
}
if h == "localhost" {
return true
}
ip := net.ParseIP(h)
return ip != nil && ip.IsLoopback()
}
func normalizeLocalHost(host string) string {
h := strings.TrimSpace(strings.ToLower(host))
switch h {
case "0.0.0.0", "::":
slog.Warn("non-loopback bind address overridden for local mode", "from", host, "to", "127.0.0.1")
return "127.0.0.1"
default:
return host
}
}
func setupLogger(cfg config.LoggingConfig) {
var level slog.Level
switch cfg.Level {
@@ -278,7 +315,7 @@ func setupDatabaseFromDSN(dsn string) (*gorm.DB, error) {
return db, nil
}
func setupRouter(cfg *config.Config, connMgr *db.ConnectionManager, mariaDB *gorm.DB, dbUser string) (*gin.Engine, error) {
func setupRouter(cfg *config.Config, configPath string, connMgr *db.ConnectionManager, mariaDB *gorm.DB, dbUser string) (*gin.Engine, error) {
var componentRepo *repository.ComponentRepository
var categoryRepo *repository.CategoryRepository
var priceRepo *repository.PriceRepository
@@ -320,6 +357,7 @@ func setupRouter(cfg *config.Config, connMgr *db.ConnectionManager, mariaDB *gor
}
router := gin.New()
router.MaxMultipartMemory = 26 << 20 // 26MB; stock import handler enforces 25MB payload limit
router.Use(gin.Recovery())
router.Use(requestLogger())
router.Use(middleware.CORS())
@@ -388,6 +426,107 @@ func setupRouter(cfg *config.Config, connMgr *db.ConnectionManager, mariaDB *gor
})
})
router.GET("/api/connection-settings", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"host": cfg.Database.Host,
"port": cfg.Database.Port,
"database": cfg.Database.Name,
"user": cfg.Database.User,
})
})
router.POST("/api/connection-settings/test", func(c *gin.Context) {
host := strings.TrimSpace(c.PostForm("host"))
database := strings.TrimSpace(c.PostForm("database"))
user := strings.TrimSpace(c.PostForm("user"))
password := c.PostForm("password")
port, err := strconv.Atoi(strings.TrimSpace(c.DefaultPostForm("port", strconv.Itoa(cfg.Database.Port))))
if err != nil || port <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "invalid port"})
return
}
if host == "" || database == "" || user == "" {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "all fields except password are required"})
return
}
if password == "" {
password = cfg.Database.Password
}
dsn := buildMySQLDSN(host, port, database, user, password, 5*time.Second)
testDB, err := gorm.Open(mysql.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 := testDB.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
}
c.JSON(http.StatusOK, gin.H{"success": true, "message": "connection successful"})
})
router.POST("/api/connection-settings", func(c *gin.Context) {
host := strings.TrimSpace(c.PostForm("host"))
database := strings.TrimSpace(c.PostForm("database"))
user := strings.TrimSpace(c.PostForm("user"))
password := c.PostForm("password")
port, err := strconv.Atoi(strings.TrimSpace(c.DefaultPostForm("port", strconv.Itoa(cfg.Database.Port))))
if err != nil || port <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "invalid port"})
return
}
if host == "" || database == "" || user == "" {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "all fields except password are required"})
return
}
if password == "" {
password = cfg.Database.Password
}
dsn := buildMySQLDSN(host, port, database, user, password, 5*time.Second)
testDB, err := gorm.Open(mysql.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 := testDB.DB()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": fmt.Sprintf("failed to get database handle: %v", err)})
return
}
if err := sqlDB.Ping(); err != nil {
_ = sqlDB.Close()
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": fmt.Sprintf("ping failed: %v", err)})
return
}
_ = sqlDB.Close()
cfg.Database.Host = host
cfg.Database.Port = port
cfg.Database.Name = database
cfg.Database.User = user
cfg.Database.Password = password
if err := saveConfig(configPath, cfg); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": fmt.Sprintf("failed to save config: %v", err)})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "settings saved, restart required",
"restart_required": true,
})
})
router.GET("/api/current-user", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"username": dbUser,
@@ -520,3 +659,30 @@ func requestLogger() gin.HandlerFunc {
)
}
}
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 saveConfig(path string, cfg *config.Config) error {
data, err := yaml.Marshal(cfg)
if err != nil {
return fmt.Errorf("marshal config: %w", err)
}
if err := os.WriteFile(path, data, 0600); err != nil {
return fmt.Errorf("write config: %w", err)
}
return nil
}