Files
vpn-panel/cmd/install/main.go
T

186 lines
5.4 KiB
Go

package main
import (
"bufio"
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"os"
"path/filepath"
"strings"
"syscall"
"golang.org/x/term"
"vpn-panel/internal/auth"
"vpn-panel/internal/database"
"vpn-panel/internal/store"
)
func main() {
fmt.Println()
fmt.Println(" ╔══════════════════════════════════════╗")
fmt.Println(" ║ VPN Panel — Установщик ║")
fmt.Println(" ║ Ядро: Xray-core ║")
fmt.Println(" ╚══════════════════════════════════════╝")
fmt.Println()
reader := bufio.NewReader(os.Stdin)
cwd, _ := os.Getwd()
envPath := filepath.Join(cwd, ".env")
if fileExists(envPath) {
fmt.Print("Файл .env уже существует. Перезаписать? [y/N]: ")
ans, _ := reader.ReadString('\n')
if strings.TrimSpace(strings.ToLower(ans)) != "y" {
fmt.Println("Установка отменена.")
return
}
}
domain := prompt(reader, "Домен панели (например panel.example.com)", "localhost")
appPort := prompt(reader, "Порт приложения", "8080")
fmt.Println("\n--- PostgreSQL 17 ---")
dbHost := prompt(reader, "Хост БД", "postgres")
dbPort := prompt(reader, "Порт БД", "5432")
dbUser := prompt(reader, "Пользователь БД", "vpnpanel")
dbPass := promptSecret("Пароль БД")
dbName := prompt(reader, "Имя базы данных", "vpnpanel")
fmt.Println("\n--- Администратор (единственный) ---")
adminEmail := prompt(reader, "Email администратора", "admin@localhost")
adminPass := promptSecret("Пароль администратора (мин. 8 символов)")
for len(adminPass) < 8 {
fmt.Println("Пароль слишком короткий.")
adminPass = promptSecret("Пароль администратора")
}
adminPass2 := promptSecret("Подтвердите пароль")
for adminPass != adminPass2 {
fmt.Println("Пароли не совпадают.")
adminPass = promptSecret("Пароль администратора")
adminPass2 = promptSecret("Подтвердите пароль")
}
secretKey, err := generateSecret()
if err != nil {
fmt.Fprintf(os.Stderr, "ошибка генерации SECRET_KEY: %v\n", err)
os.Exit(1)
}
databaseURL := fmt.Sprintf(
"postgres://%s:%s@%s:%s/%s?sslmode=disable",
dbUser, urlEncode(dbPass), dbHost, dbPort, dbName,
)
fmt.Println("\n--- Проверка подключения к БД ---")
ctx := context.Background()
pool, err := database.Connect(ctx, databaseURL)
if err != nil {
fmt.Fprintf(os.Stderr, "не удалось подключиться: %v\n", err)
fmt.Println("Запустите PostgreSQL (docker compose up -d postgres) и повторите установку.")
os.Exit(1)
}
defer pool.Close()
if err := database.Migrate(ctx, pool); err != nil {
fmt.Fprintf(os.Stderr, "миграции: %v\n", err)
os.Exit(1)
}
hash, err := auth.HashPassword(adminPass)
if err != nil {
fmt.Fprintf(os.Stderr, "хеш пароля: %v\n", err)
os.Exit(1)
}
users := store.NewUserStore(pool)
has, _ := users.HasAdmin(ctx)
if has {
fmt.Println("Администратор уже есть в БД — пропускаем создание.")
} else {
u, err := users.CreateAdmin(ctx, strings.ToLower(strings.TrimSpace(adminEmail)), hash)
if err != nil {
fmt.Fprintf(os.Stderr, "создание админа: %v\n", err)
os.Exit(1)
}
fmt.Printf("Администратор создан: %s (%s)\n", u.Email, u.ID)
}
envContent := fmt.Sprintf(`# Сгенерировано установщиком VPN Panel
APP_PORT=%s
APP_DOMAIN=%s
DATABASE_URL=%s
SECRET_KEY=%s
INSTALLED=true
# PostgreSQL (для docker-compose)
POSTGRES_USER=%s
POSTGRES_PASSWORD=%s
POSTGRES_DB=%s
POSTGRES_HOST=%s
POSTGRES_PORT=%s
`,
appPort, domain, databaseURL, secretKey,
dbUser, dbPass, dbName, dbHost, dbPort,
)
if err := os.WriteFile(envPath, []byte(envContent), 0600); err != nil {
fmt.Fprintf(os.Stderr, "запись .env: %v\n", err)
os.Exit(1)
}
fmt.Println()
fmt.Println(" ✓ Установка завершена")
fmt.Println(" ✓ Файл .env создан")
fmt.Println()
fmt.Println(" Дальше:")
fmt.Println(" docker compose up -d")
fmt.Println(" или: go run ./cmd/panel")
fmt.Println()
fmt.Printf(" Панель: http://%s:%s\n", domain, appPort)
fmt.Println()
}
func prompt(reader *bufio.Reader, label, defaultVal string) string {
if defaultVal != "" {
fmt.Printf("%s [%s]: ", label, defaultVal)
} else {
fmt.Printf("%s: ", label)
}
line, _ := reader.ReadString('\n')
line = strings.TrimSpace(line)
if line == "" {
return defaultVal
}
return line
}
func promptSecret(label string) string {
fmt.Printf("%s: ", label)
b, err := term.ReadPassword(int(syscall.Stdin))
fmt.Println()
if err != nil {
return ""
}
return string(b)
}
func generateSecret() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(b), nil
}
func fileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
func urlEncode(s string) string {
r := strings.NewReplacer(":", "%3A", "@", "%40", "/", "%2F")
return r.Replace(s)
}