core/backend/main.go
enzo 5836f2201a feat: add Services and Logs modules (systemctl + journalctl via SSH)
Backend:
- modules/services: list, status, start/stop/restart systemctl services
  with pct exec support for LXC targets
- modules/logs: journalctl unit listing + WebSocket live streaming
  (direct SSH connection, journalctl -f, graceful teardown on WS close)
- migrations/003: seed services and logs modules in DB
- main.go: register services.New() and logs.New() in module loader

Frontend:
- services.html: target selector, search/filter, services table with
  active state indicators and start/stop/restart buttons
- logs.html: target + unit selectors, live follow toggle, scrollable
  terminal output with 3000-line cap
- app.js: servicePage() and logsPage() Alpine components + navItems
- locales: services and logs i18n keys (fr + en)
- pages.css: services table, state dots, logs output styles

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 02:03:55 +01:00

226 lines
9 KiB
Go

// 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
}