3419d90e61
Co-authored-by: Cursor <cursoragent@cursor.com>
224 lines
5.7 KiB
Go
224 lines
5.7 KiB
Go
package setup
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
type Config struct {
|
|
SiteDomain string
|
|
AcmeEmail 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.AcmeEmail = ask(in, "Email для Let's Encrypt (Traefik)", "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, traefik/dynamic/shop.yml")
|
|
fmt.Println("\nДальше (на сервере):")
|
|
fmt.Println(" docker compose up --build -d")
|
|
fmt.Println(" ./check.sh --after-start")
|
|
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")
|
|
traefikPath := filepath.Join(root, "traefik", "dynamic", "shop.yml")
|
|
|
|
if err := os.WriteFile(envPath, []byte(buildEnv(cfg)), 0o600); err != nil {
|
|
return fmt.Errorf("write .env: %w", err)
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(traefikPath), 0o755); err != nil {
|
|
return err
|
|
}
|
|
if err := os.WriteFile(traefikPath, []byte(buildTraefikDynamic(cfg)), 0o644); err != nil {
|
|
return fmt.Errorf("write traefik config: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func buildEnv(cfg Config) string {
|
|
dbURL := DatabaseURL(cfg)
|
|
cookieSecure := "false"
|
|
if !useLocalDomain(cfg.SiteDomain) {
|
|
cookieSecure = "true"
|
|
}
|
|
lines := []string{
|
|
"# Сгенерировано установщиком ShopNova",
|
|
fmt.Sprintf("SITE_DOMAIN=%s", cfg.SiteDomain),
|
|
fmt.Sprintf("ACME_EMAIL=%s", cfg.AcmeEmail),
|
|
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("COOKIE_SECURE=%s", cookieSecure),
|
|
"SESSION_TTL_HOURS=168",
|
|
"",
|
|
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 buildTraefikDynamic(cfg Config) string {
|
|
domain := strings.TrimSpace(cfg.SiteDomain)
|
|
if useLocalDomain(domain) {
|
|
return fmt.Sprintf(`# Сгенерировано установщиком ShopNova (localhost)
|
|
http:
|
|
routers:
|
|
shop:
|
|
rule: "Host(`+"`%s`"+`)"
|
|
entryPoints: [web]
|
|
middlewares: [gzip]
|
|
service: shop
|
|
middlewares:
|
|
gzip:
|
|
compress: {}
|
|
services:
|
|
shop:
|
|
loadBalancer:
|
|
servers:
|
|
- url: "http://app:8080"
|
|
`, domain)
|
|
}
|
|
|
|
return fmt.Sprintf(`# Сгенерировано установщиком ShopNova
|
|
http:
|
|
routers:
|
|
shop-http:
|
|
rule: "Host(`+"`%s`"+`)"
|
|
entryPoints: [web]
|
|
middlewares: [redirect-https]
|
|
service: shop
|
|
shop:
|
|
rule: "Host(`+"`%s`"+`)"
|
|
entryPoints: [websecure]
|
|
middlewares: [gzip]
|
|
service: shop
|
|
tls:
|
|
certResolver: letsencrypt
|
|
middlewares:
|
|
redirect-https:
|
|
redirectScheme:
|
|
scheme: https
|
|
permanent: true
|
|
gzip:
|
|
compress: {}
|
|
services:
|
|
shop:
|
|
loadBalancer:
|
|
servers:
|
|
- url: "http://app:8080"
|
|
`, 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")
|
|
}
|
|
}
|