Initial commit: VPN panel on Go, PostgreSQL 17, Docker, Xray-core
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"vpn-panel/internal/config"
|
||||
"vpn-panel/internal/database"
|
||||
"vpn-panel/internal/handlers"
|
||||
"vpn-panel/web"
|
||||
)
|
||||
|
||||
func main() {
|
||||
_ = godotenv.Load()
|
||||
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
log.Fatalf("конфигурация: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
pool, err := database.Connect(ctx, cfg.DatabaseURL)
|
||||
if err != nil {
|
||||
log.Fatalf("база данных: %v", err)
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
if err := database.Migrate(ctx, pool); err != nil {
|
||||
log.Fatalf("миграции: %v", err)
|
||||
}
|
||||
|
||||
h, err := handlers.New(cfg, pool)
|
||||
if err != nil {
|
||||
log.Fatalf("handlers: %v", err)
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /", h.Home)
|
||||
mux.HandleFunc("GET /health", h.Health)
|
||||
mux.HandleFunc("GET /register", h.RegisterAdmin)
|
||||
mux.HandleFunc("POST /register", h.RegisterAdmin)
|
||||
mux.HandleFunc("GET /login", h.Login)
|
||||
mux.HandleFunc("POST /login", h.Login)
|
||||
mux.HandleFunc("GET /logout", h.Logout)
|
||||
|
||||
staticFS, err := fs.Sub(web.Static, "static")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))))
|
||||
|
||||
addr := fmt.Sprintf(":%d", cfg.AppPort)
|
||||
srv := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Printf("VPN Panel запущена на http://%s%s", cfg.AppDomain, addr)
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
_ = srv.Shutdown(shutdownCtx)
|
||||
log.Println("остановлена")
|
||||
}
|
||||
Reference in New Issue
Block a user