package setup import ( "bufio" "fmt" "net/url" "os" "path/filepath" "strings" ) type Config struct { SiteDomain string CaddyEmail string HTTPPort string HTTPSPort string UseDockerDB bool DBHost string DBPort string DBUser string DBPassword string DBName string DBSSLMode string } func RunInteractive(root string) (Config, error) { in := bufio.NewReader(os.Stdin) fmt.Println("=== Установщик ShopNova ===") fmt.Println() cfg := Config{} cfg.SiteDomain = ask(in, "Домен сайта (например shop.example.com, Enter = localhost)", "localhost") cfg.CaddyEmail = ask(in, "Email для Let's Encrypt (Caddy)", "admin@localhost") cfg.HTTPPort = ask(in, "HTTP порт", "80") cfg.HTTPSPort = ask(in, "HTTPS порт", "443") useDocker := askYesNo(in, "Использовать PostgreSQL из Docker Compose?", true) cfg.UseDockerDB = useDocker if useDocker { cfg.DBHost = "postgres" cfg.DBPort = "5432" cfg.DBSSLMode = "require" fmt.Println("\n--- База данных (контейнер postgres) ---") } else { fmt.Println("\n--- База данных (внешний сервер) ---") cfg.DBHost = ask(in, "Хост БД", "localhost") cfg.DBPort = ask(in, "Порт БД", "5432") cfg.DBSSLMode = ask(in, "SSL mode (disable|require|verify-full)", "require") } cfg.DBUser = ask(in, "Пользователь БД", "shop") cfg.DBPassword = askPassword(in, "Пароль БД") cfg.DBName = ask(in, "Имя базы данных", "shopdb") if err := WriteFiles(root, cfg); err != nil { return cfg, err } fmt.Println("\n✓ Созданы файлы: .env, caddy/Caddyfile") fmt.Println("\nДальше:") fmt.Println(" docker compose up --build -d") if !useLocalDomain(cfg.SiteDomain) { fmt.Printf(" Сайт: https://%s\n", cfg.SiteDomain) } else { fmt.Printf(" Сайт: http://localhost:%s\n", cfg.HTTPPort) } return cfg, nil } func WriteFiles(root string, cfg Config) error { envPath := filepath.Join(root, ".env") caddyPath := filepath.Join(root, "caddy", "Caddyfile") if err := os.WriteFile(envPath, []byte(buildEnv(cfg)), 0o600); err != nil { return fmt.Errorf("write .env: %w", err) } if err := os.MkdirAll(filepath.Dir(caddyPath), 0o755); err != nil { return err } if err := os.WriteFile(caddyPath, []byte(buildCaddyfile(cfg)), 0o644); err != nil { return fmt.Errorf("write Caddyfile: %w", err) } return nil } func buildEnv(cfg Config) string { dbURL := DatabaseURL(cfg) lines := []string{ "# Сгенерировано установщиком ShopNova", fmt.Sprintf("SITE_DOMAIN=%s", cfg.SiteDomain), fmt.Sprintf("CADDY_EMAIL=%s", cfg.CaddyEmail), fmt.Sprintf("HTTP_PORT=%s", cfg.HTTPPort), fmt.Sprintf("HTTPS_PORT=%s", cfg.HTTPSPort), "", fmt.Sprintf("POSTGRES_USER=%s", cfg.DBUser), fmt.Sprintf("POSTGRES_PASSWORD=%s", cfg.DBPassword), fmt.Sprintf("POSTGRES_DB=%s", cfg.DBName), "", fmt.Sprintf("DATABASE_URL=%s", dbURL), "APP_PORT=8080", "", fmt.Sprintf("DB_HOST=%s", cfg.DBHost), fmt.Sprintf("DB_PORT=%s", cfg.DBPort), fmt.Sprintf("DB_SSLMODE=%s", cfg.DBSSLMode), } return strings.Join(lines, "\n") + "\n" } func DatabaseURL(cfg Config) string { u := &url.URL{ Scheme: "postgres", User: url.UserPassword(cfg.DBUser, cfg.DBPassword), Host: fmt.Sprintf("%s:%s", cfg.DBHost, cfg.DBPort), Path: cfg.DBName, } q := u.Query() q.Set("sslmode", cfg.DBSSLMode) u.RawQuery = q.Encode() return u.String() } func buildCaddyfile(cfg Config) string { email := cfg.CaddyEmail if useLocalDomain(cfg.SiteDomain) { return fmt.Sprintf(`{ email %s } :80 { encode gzip zstd @api path /health /version handle @api { reverse_proxy app:8080 } handle /static/* { reverse_proxy app:8080 } handle { reverse_proxy app:8080 } log { output stdout format console } } `, email) } domain := strings.TrimSpace(cfg.SiteDomain) return fmt.Sprintf(`{ email %s } %s { encode gzip zstd @api path /health /version handle @api { reverse_proxy app:8080 } handle /static/* { reverse_proxy app:8080 } handle { reverse_proxy app:8080 } log { output stdout format console } } http://%s { redir https://{host}{uri} permanent } `, email, domain, domain) } func useLocalDomain(d string) bool { d = strings.ToLower(strings.TrimSpace(d)) return d == "" || d == "localhost" || d == "127.0.0.1" || d == "local" } func ask(in *bufio.Reader, prompt, def string) string { if def != "" { fmt.Printf("%s [%s]: ", prompt, def) } else { fmt.Printf("%s: ", prompt) } line, _ := in.ReadString('\n') line = strings.TrimSpace(line) if line == "" { return def } return line } func askPassword(in *bufio.Reader, prompt string) string { fmt.Printf("%s: ", prompt) line, _ := in.ReadString('\n') return strings.TrimSpace(line) } func askYesNo(in *bufio.Reader, prompt string, def bool) bool { defStr := "y" if !def { defStr = "n" } for { ans := strings.ToLower(ask(in, prompt+" (y/n)", defStr)) switch ans { case "y", "yes", "д", "да": return true case "n", "no", "н", "нет": return false } fmt.Println("Введите y или n") } }