Restore connection settings modal from top nav
This commit is contained in:
170
cmd/pfs/main.go
170
cmd/pfs/main.go
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user