package main import ( "context" "log" "net/http" "os" "os/signal" "path/filepath" "syscall" "time" "github.com/evilfox/wg-panel/internal/auth" "github.com/evilfox/wg-panel/internal/config" "github.com/evilfox/wg-panel/internal/db" "github.com/evilfox/wg-panel/internal/handlers" "github.com/evilfox/wg-panel/internal/mysqlimport" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/jackc/pgx/v5/pgxpool" ) func main() { cfg := config.Load() ctx := context.Background() root := envOr("APP_ROOT", ".") migrationsDir := envOr("MIGRATIONS_DIR", filepath.Join(root, "migrations")) templatesDir := envOr("TEMPLATES_DIR", filepath.Join(root, "web", "templates")) staticDir := envOr("STATIC_DIR", filepath.Join(root, "web", "static")) pool, err := waitForDB(ctx, cfg.DatabaseURL, 60*time.Second) if err != nil { log.Fatalf("database: %v", err) } defer pool.Close() if err := db.Migrate(ctx, pool, migrationsDir); err != nil { log.Fatalf("migrate: %v", err) } if cfg.AutoImportDump { if _, err := os.Stat(cfg.MySQLDumpPath); err == nil { if err := mysqlimport.ImportFile(ctx, pool, cfg.MySQLDumpPath); err != nil { log.Fatalf("mysql import: %v", err) } } else { log.Printf("mysql dump not found at %s, skip import", cfg.MySQLDumpPath) } } authStore := auth.NewStore(cfg, pool) if err := authStore.EnsureAdmin(ctx); err != nil { log.Fatalf("ensure admin: %v", err) } log.Printf("admin ready: username=%s email=%s", cfg.AdminUsername, cfg.AdminEmail) app, err := handlers.New(pool, authStore, cfg, templatesDir) if err != nil { log.Fatalf("handlers: %v", err) } r := chi.NewRouter() r.Use(middleware.RequestID) r.Use(middleware.RealIP) r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Use(app.OfflineMiddleware) fileServer(r, "/static/", http.Dir(staticDir)) r.Get("/", app.Home) r.Get("/healthz", func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "text/plain") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("ok")) }) r.Get("/login", app.LoginPage) r.Post("/login", app.LoginPost) r.Get("/logout", app.Logout) r.Get("/wg", app.PublicWG) r.Get("/wg_config.php", app.PublicWG) r.Route("/admin", func(r chi.Router) { r.Use(authStore.RequireAdmin) r.Get("/", app.AdminDashboard) r.Post("/status", app.AdminStatusPost) r.Get("/wg", app.AdminWG) r.Post("/wg/upload", app.AdminWGUpload) r.Post("/wg/delete", app.AdminWGDelete) r.Post("/wg/settings", app.AdminWGSettings) }) srv := &http.Server{ Addr: ":" + cfg.AppPort, Handler: r, ReadHeaderTimeout: 10 * time.Second, } go func() { log.Printf("listening on :%s (%s)", cfg.AppPort, cfg.AppURL) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("server: %v", err) } }() stop := make(chan os.Signal, 1) signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) <-stop shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() _ = srv.Shutdown(shutdownCtx) } func waitForDB(ctx context.Context, databaseURL string, timeout time.Duration) (*pgxpool.Pool, error) { deadline := time.Now().Add(timeout) var last error for time.Now().Before(deadline) { pool, err := db.Connect(ctx, databaseURL) if err == nil { return pool, nil } last = err log.Printf("waiting for postgres: %v", err) time.Sleep(2 * time.Second) } return nil, last } func fileServer(r chi.Router, pathPrefix string, root http.FileSystem) { r.Handle(pathPrefix+"*", http.StripPrefix(pathPrefix, http.FileServer(root))) } func envOr(k, def string) string { if v := os.Getenv(k); v != "" { return v } return def }