// ProxmoxPanel — CORE Backend // Point d'entrée du serveur Go. Initialise la base de données, les services, // enregistre les modules actifs et démarre le serveur HTTP sur :3001. package main import ( "io" "log" "net/http" "os" "git.geronzi.fr/proxmoxPanel/core/backend/internal/api" "git.geronzi.fr/proxmoxPanel/core/backend/internal/audit" "git.geronzi.fr/proxmoxPanel/core/backend/internal/auth" "git.geronzi.fr/proxmoxPanel/core/backend/internal/crypto" "git.geronzi.fr/proxmoxPanel/core/backend/internal/db" "git.geronzi.fr/proxmoxPanel/core/backend/internal/logbuffer" sshpool "git.geronzi.fr/proxmoxPanel/core/backend/internal/ssh" "git.geronzi.fr/proxmoxPanel/core/backend/internal/websocket" "git.geronzi.fr/proxmoxPanel/core/backend/modules" "git.geronzi.fr/proxmoxPanel/core/backend/modules/logs" "git.geronzi.fr/proxmoxPanel/core/backend/modules/services" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" ) func main() { // Brancher le buffer de logs (stderr + mémoire) avant tout autre log log.SetOutput(io.MultiWriter(os.Stderr, logbuffer.Global)) // Répertoire de données persistantes (volume Docker) dataDir := getEnv("DATA_DIR", "/app/data") log.Printf("ProxmoxPanel CORE — démarrage (data: %s)", dataDir) // ── Initialisation de la base de données ─────────────────────────────── database, err := db.Open(dataDir) if err != nil { log.Fatalf("Impossible d'ouvrir la base de données : %v", err) } log.Println("Base de données SQLite initialisée") // ── Services de base ─────────────────────────────────────────────────── encryptor, err := crypto.NewEncryptor(dataDir) if err != nil { log.Fatalf("Impossible d'initialiser le chiffrement : %v", err) } log.Println("Chiffrement AES-256-GCM initialisé") jwtManager, err := auth.NewJWTManager(dataDir) if err != nil { log.Fatalf("Impossible d'initialiser JWT : %v", err) } log.Println("Clés JWT RS256 prêtes") // SSH host depuis la configuration (peut être vide si pas encore installé) sshHost, _, _ := database.GetSetting("ssh_host") var sshAuthenticator *auth.SSHAuthenticator if sshHost != "" { sshAuthenticator = auth.NewSSHAuthenticator(sshHost) } else { sshAuthenticator = auth.NewSSHAuthenticator("") // Sera mis à jour après installation } sshPool := sshpool.NewPool() defer sshPool.Close() hub := websocket.NewHub() auditLogger := audit.New(database.DB) // ── Chargement des modules actifs ────────────────────────────────────── loader := modules.NewLoader(database.DB) loader.RegisterModule(services.New(database, sshPool, encryptor)) loader.RegisterModule(logs.New(database, sshPool, encryptor)) if err := loader.LoadActive(); err != nil { log.Fatalf("Erreur chargement modules : %v", err) } // ── Handlers HTTP ────────────────────────────────────────────────────── installHandler := api.NewInstallHandler(database, encryptor) authHandler := api.NewAuthHandler(database, jwtManager, sshAuthenticator, auditLogger) proxmoxHandler := api.NewProxmoxHandler(database, hub, auditLogger, encryptor) updatesHandler := api.NewUpdatesHandler(database, sshPool, hub, auditLogger, encryptor) settingsHandler := api.NewSettingsHandler(database, auditLogger, encryptor) terminalHandler := api.NewTerminalHandler(database, auditLogger, encryptor) // Démarrer le polling Proxmox en arrière-plan proxmoxHandler.StartPolling() // ── Router Chi ───────────────────────────────────────────────────────── r := chi.NewRouter() // Middlewares globaux r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Use(middleware.RequestID) r.Use(api.SecurityHeaders) r.Use(middleware.Compress(5)) // Compression gzip // Limiter global (100 req/min par IP) globalLimiter := api.NewRateLimiter(100, 60*1000000000) // 60 secondes r.Use(api.RateLimit(globalLimiter)) // ── Routes publiques (sans authentification) ─────────────────────────── r.Get("/api/health", func(w http.ResponseWriter, r *http.Request) { api.JSONResponse(w, http.StatusOK, map[string]string{"status": "ok"}) }) // Routes d'installation (accessibles seulement si non-installé) r.Group(func(r chi.Router) { r.Use(requireNotInstalled(database)) r.Get("/api/install/status", installHandler.GetStatus) r.Post("/api/install/test-ssh", installHandler.TestSSH) r.Post("/api/install/test-proxmox", installHandler.TestProxmoxToken) r.Post("/api/install/configure", installHandler.Configure) }) // Status d'installation accessible toujours (pour la redirection frontend) r.Get("/api/install/check", func(w http.ResponseWriter, r *http.Request) { installed, _ := database.IsInstalled() api.JSONResponse(w, http.StatusOK, map[string]bool{"installed": installed}) }) // Routes d'authentification r.Post("/api/auth/login", authHandler.Login) r.Post("/api/auth/refresh", authHandler.Refresh) // ── Routes protégées (JWT requis) ────────────────────────────────────── r.Group(func(r chi.Router) { r.Use(api.RequireAuth(jwtManager)) r.Post("/api/auth/logout", authHandler.Logout) r.Get("/api/auth/me", authHandler.Me) r.Patch("/api/auth/preferences", authHandler.UpdatePreferences) r.Get("/api/auth/sessions", authHandler.GetSessions) r.Delete("/api/auth/sessions/{id}", authHandler.RevokeSession) // Proxmox r.Get("/api/proxmox/resources", proxmoxHandler.GetResources) r.Get("/api/proxmox/lxc", proxmoxHandler.GetLXC) // Actions Proxmox — admin uniquement r.Group(func(r chi.Router) { r.Use(api.RequireAdmin) r.Post("/api/proxmox/lxc/{vmid}/start", proxmoxHandler.StartLXC) r.Post("/api/proxmox/lxc/{vmid}/stop", proxmoxHandler.StopLXC) }) // Mises à jour — admin uniquement r.Group(func(r chi.Router) { r.Use(api.RequireAdmin) r.Post("/api/updates/run", updatesHandler.RunUpdate) r.Get("/api/updates/targets", updatesHandler.GetTargets) r.Get("/api/updates/packages", updatesHandler.GetPackages) }) r.Get("/api/updates/history", updatesHandler.GetHistory) // Paramètres — admin uniquement r.Group(func(r chi.Router) { r.Use(api.RequireAdmin) r.Get("/api/settings", settingsHandler.GetAll) r.Put("/api/settings/{key}", settingsHandler.UpdateSetting) r.Get("/api/settings/audit", settingsHandler.GetAuditLog) r.Get("/api/settings/logs", settingsHandler.GetLogs) }) // Modules r.Get("/api/modules", settingsHandler.GetModules) r.Group(func(r chi.Router) { r.Use(api.RequireAdmin) r.Post("/api/modules/{id}/enable", settingsHandler.EnableModule) r.Post("/api/modules/{id}/disable", settingsHandler.DisableModule) }) // WebSocket — les routes WS extraient le token via query param r.Get("/ws/proxmox", proxmoxHandler.WebSocket) r.Get("/ws/updates/{jobId}", updatesHandler.WebSocketUpdate) r.Get("/ws/terminal", terminalHandler.WebSocket) }) // Routes enregistrées par les modules actifs for _, route := range loader.Registry().GetRoutes() { routeCopy := route // Capturer la variable pour la closure if routeCopy.RequireAdmin { r.With(api.RequireAuth(jwtManager), api.RequireAdmin).MethodFunc(routeCopy.Method, routeCopy.Path, routeCopy.Handler) } else { r.With(api.RequireAuth(jwtManager)).MethodFunc(routeCopy.Method, routeCopy.Path, routeCopy.Handler) } } // Servir les assets frontend (en production, c'est Nginx qui s'en charge) if _, err := os.Stat("./static"); err == nil { fs := http.FileServer(http.Dir("./static")) r.Handle("/*", fs) } // ── Démarrage du serveur ─────────────────────────────────────────────── addr := getEnv("LISTEN_ADDR", ":3001") log.Printf("Serveur démarré sur %s", addr) if err := http.ListenAndServe(addr, r); err != nil { log.Fatalf("Serveur arrêté : %v", err) } } // requireNotInstalled est un middleware qui bloque les routes d'installation si déjà installé. func requireNotInstalled(database *db.DB) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // La route /api/install/status reste accessible pour le check installed, _ := database.IsInstalled() if installed { api.JSONError(w, "Application déjà installée", http.StatusForbidden) return } next.ServeHTTP(w, r) }) } } // getEnv lit une variable d'environnement avec une valeur par défaut. func getEnv(key, defaultValue string) string { if v := os.Getenv(key); v != "" { return v } return defaultValue }